diff --git a/README.md b/README.md index 96377c4..325774c 100644 --- a/README.md +++ b/README.md @@ -48,29 +48,21 @@ Buba is configured completely through environment variables: ## Creating Animation Plugins Buba instantiates objects of type `BubaAnimation` and runs through them in a loop. Each animation must implement the -`show()` method, which should send data to the display. The animation is run in its own thread, therefore, the animation +`run()` method, which should send data to the display. The animation is run in its own thread, therefor, the animation should sleep an appropriate time to let users take in the information. See the existing animations in [buba/animations](./buba/animations) for inspiration. -To implement your own animation, subclass [BubaAnimation](buba/bubaanimation.py). - -In the `__init__()` method, you will want to set `self.title`, and configure your data source. - -Implement `update()` to fetch any data and fill in `self.rows` (list of rows). Each row in `self.rows` is a list of -values for the columns in the layout. - -The default layout has a wide, left-aligned column for text, and a short, right-aligned column for a time period or a -value. You can define your own layout if necessary. - -If you do not want to show tabular data, you can override `show()` and implement sending text to the display yourself. +Note: if you need to fetch and update external information regularly, you should start your own thread when initalizing +your animation. ## Character Set The display uses [Code Page 437](https://en.wikipedia.org/wiki/Code_page_437), with a few exceptions. Due to the limited resolution of the segments, the display can deviate significantly. -Note that the [Python codecs](https://docs.python.org/3/library/codecs.html) for `CP437` do not map all special -characters correctly. +Note that the [Python codecs](https://docs.python.org/3/library/codecs.html) for `CP437` do not map all special characters correctly. + + | Code | CP437 | Geavision Spaltenschrift | |------|--------------|--------------------------| diff --git a/buba/__main__.py b/buba/__main__.py index 57b8432..6b0f1d6 100644 --- a/buba/__main__.py +++ b/buba/__main__.py @@ -1,12 +1,14 @@ import logging -from bottle import Bottle, static_file, TEMPLATE_PATH, jinja2_view, request, HTTPResponse +from bottle import Bottle, static_file, TEMPLATE_PATH, jinja2_view from bottle_log import LoggingPlugin from bottle_websocket import websocket, GeventWebSocketServer from geventwebsocket.websocket import WebSocket -from buba.animationconfig import setup_animations -from buba.animations.webqueue import WebQueue +from buba.animations.dbf import DBFAnimation +from buba.animations.icalevents import IcalEvents +from buba.animations.snake import SnakeAnimation +from buba.animations.time import BubaTime from buba.appconfig import AppConfig from buba.bubaanimator import BubaAnimator from buba.bubacmd import BubaCmd @@ -26,9 +28,13 @@ TEMPLATE_PATH.insert(0, config.templatepath) websocket_clients = WebSocketClients() buba = BubaCmd(config.serial, websocket_clients.send) animator = BubaAnimator(buba) -webqueue = WebQueue(buba) -setup_animations(config, animator, webqueue) - +# animator.add(BubaCharset) #, single=12) +animator.add(BubaTime) +animator.add(DBFAnimation, ds100="AHST", station="Holstenstraße") +animator.add(DBFAnimation, ds100="AHS", station="Altona", count=9) +animator.add(IcalEvents, url="https://cloud.hamburg.ccc.de/remote.php/dav/public-calendars/QJAdExziSnNJEz5g?export", + title="CCCHH Events") +animator.add(SnakeAnimation) @app.route("/static/") def server_static(filepath): @@ -38,9 +44,7 @@ def server_static(filepath): @app.get("/") @jinja2_view("home.html.j2") def root(): - return { - "user_forms": range(webqueue.maxrows), - } + return {} @app.get('/ws', apply=[websocket]) @@ -57,49 +61,6 @@ def websocket_endpoint(ws: WebSocket): websocket_clients.remove(ws) -def json_error(status: str, body: dict) -> HTTPResponse: - return HTTPResponse( - body=body, - status=status, - headers={ - "Content-Type": "application/json"} - ) - - -def json_error_400(error: str) -> HTTPResponse: - return json_error("400 Bad Request", { - "status": "error", - "error": error, - }) - - -@app.post("/user-entry") -def user_entry(): - log.debug(f"json: {request.json}") - title = request.json.get("title") - if not isinstance(title, str): - raise json_error_400("title must be a string") - title = title.strip() - if title == "": - raise json_error_400("title must not be empty") - if not isinstance(request.json.get("rows"), list): - raise json_error_400("rows must be a list") - rows = [] - for row in request.json.get("rows"): - if not isinstance(row[0], str) or not isinstance(row[0], str): - raise json_error_400("rows elements must be strings") - row = [v.strip() for v in row] - if row[0] != "" or row[1] != "": - rows.append(row) - rows = rows[:webqueue.maxrows] - if webqueue.add(title, rows): - return { - "status": "success", - } - else: - raise json_error_400("queue is full") - - if __name__ == '__main__': app.run(host=config.listen_host, port=config.listen_port, server=GeventWebSocketServer, debug=config.debug, quiet=not config.debug) diff --git a/buba/animationconfig.py b/buba/animationconfig.py deleted file mode 100644 index c8a7584..0000000 --- a/buba/animationconfig.py +++ /dev/null @@ -1,58 +0,0 @@ -import datetime -import logging - -from buba.animations.charset import BubaCharset -from buba.animations.dbf import DBF -from buba.animations.homeassistant import HomeAssistant -from buba.animations.icalevents import IcalEvents -from buba.animations.snake import SnakeAnimation -from buba.animations.spaceapi import Spaceapi -from buba.animations.time import BubaTime -from buba.animations.webqueue import WebQueue -from buba.bubaanimator import BubaAnimator - -LOG = logging.getLogger(__name__) - - -def setup_animations(config, animator: BubaAnimator, webqueue: WebQueue): - cs = BubaCharset(animator.buba) - - bt = BubaTime(animator.buba, config.ip) - - snake = SnakeAnimation(animator.buba) - - dbf_ahst = DBF(animator.buba, ds100="AHST", station="Holstenstraße") - dbf_ahs = DBF(animator.buba, ds100="AHS", station="Altona", count=9) - - ccchh_events = IcalEvents(animator.buba, - url="https://cloud.hamburg.ccc.de/remote.php/dav/public-calendars/QJAdExziSnNJEz5g?export", - title="CCCHH Events") - ccc_events = IcalEvents(animator.buba, - url="https://events.ccc.de/calendar/events.ics", - title="CCC Events", range=datetime.timedelta(weeks=8)) - - fux_lichtspiele_events = IcalEvents(animator.buba, - url="https://cloud.hamburg.ccc.de/remote.php/dav/public-calendars/wdyaFEMeJiAK2jGc?export", - title="fux Lichtspiele", range=datetime.timedelta(weeks=8)) - - ccchh_spaceapi = Spaceapi(animator.buba, "https://spaceapi.hamburg.ccc.de", "CCCHH") - - ca = None - if config.clubassistant_token is not None: - ca = HomeAssistant(animator.buba, "https://club-assistant.ccchh.net", "Club Assistant", - config.clubassistant_token) - else: - LOG.warning("Club Assistant token not set, not activating animation") - - # animator.add(cs) - animator.add(bt) - animator.add(snake) - animator.add(dbf_ahst) - animator.add(dbf_ahs) - animator.add(ccchh_events) - animator.add(ccc_events) - animator.add(fux_lichtspiele_events) - animator.add(ccchh_spaceapi) - if ca is not None: - animator.add(ca) - animator.add(webqueue) diff --git a/buba/animations/charset.py b/buba/animations/charset.py index 8c283ba..ce81518 100644 --- a/buba/animations/charset.py +++ b/buba/animations/charset.py @@ -1,6 +1,6 @@ from time import sleep -from buba.bubaanimation import BubaAnimation +from buba.bubaanimator import BubaAnimation from buba.bubacmd import BubaCmd @@ -10,7 +10,7 @@ class BubaCharset(BubaAnimation): self.charset = bytes(range(256)).decode("CP437") self.single = single - def show(self): + def run(self): if self.single is not None: while True: self.render(self.single) diff --git a/buba/animations/dbf.py b/buba/animations/dbf.py index e55b33b..9c32980 100644 --- a/buba/animations/dbf.py +++ b/buba/animations/dbf.py @@ -4,64 +4,71 @@ Pull departure info from https://trains.xatlabs.com and display. See also https://github.com/derf/db-fakedisplay/blob/main/README.md """ import datetime -import logging from threading import Thread from time import sleep from deutschebahn.db_infoscreen import DBInfoscreen -from buba.bubaanimator import LineLayoutColumn -from buba.bubaanimation import BubaAnimation +from buba.bubaanimator import BubaAnimation from buba.bubacmd import BubaCmd -LOG = logging.getLogger(__name__) - -class DBF(BubaAnimation): - dbf_layout = [ - LineLayoutColumn(12, BubaCmd.ALIGN_LEFT), - LineLayoutColumn(81, BubaCmd.ALIGN_LEFT), - LineLayoutColumn(27, BubaCmd.ALIGN_RIGHT), - ] +class DBFAnimation(BubaAnimation): def __init__(self, buba: BubaCmd, ds100="AHST", station="Holstenstraße", count=3): super().__init__(buba) self.dbi = DBInfoscreen("trains.xatlabs.com") self.ds100 = ds100 - self.title = station - self.layout = self.dbf_layout + self.station = station self.trains = [] self.count = count + Thread(target=self.update, daemon=True).start() def __repr__(self): return f"<{type(self).__name__}, {self.ds100}>" - def update(self): + def fetch(self): trains = self.dbi.calc_real_times(self.dbi.get_trains(self.ds100)) # Station # trains = [t for t in trains] # if t['platform'] == "1"] # platform gleis trains.sort(key=self.dbi.time_sort) self.trains = trains self.log.info(f"Fetched {len(trains)} trains") - @staticmethod - def countdown(dt: datetime, cancelled:bool): - if cancelled: - return "--" - now = datetime.datetime.now().astimezone() + def update(self): try: - day = now.strftime("%y-%m-%d") - departure = datetime.datetime.strptime(f"{day} {dt}", "%y-%m-%d %H:%M").astimezone() - except ValueError as e: + self.fetch() + except Exception as e: + self.log.warning(f"Unable to fetch {self.station}: {e}") + pass + sleep(60) + + @staticmethod + def countdown(dt: datetime): + now = datetime.datetime.now() + try: + dep_time = datetime.datetime.strptime(dt, "%H:%M").time() + except TypeError as e: return "--" - dep_td = departure - now - if dep_td.total_seconds() <= -3600: - # dep_date += datetime.timedelta(days=1) - departure += datetime.timedelta(days=1) - if dep_td.total_seconds() >= 3600 * 23: - # dep_date -= datetime.timedelta(days=1) - departure -= datetime.timedelta(days=1) + # First, assume all departure times are on the current day + dep_date = now.date() - return BubaAnimation.countdown(departure) + # Calculate timedelta under the above assumption + dep_td = datetime.datetime.combine(dep_date, dep_time) - now + + # If the calculated timedelta is more than one hour in the past, + # assume that the day should actually be the next day + # (This will be the case e.g. when a train departs at 00:15 and it's currently 23:50) + if dep_td.total_seconds() <= -3600: + dep_date += datetime.timedelta(days=1) + + # If the calculated timedelta is more than 23 hours in the future, + # assume that the day should actually be the previous day. + # (This will be the case e.g. when a train should have departed at 23:50 but it's already 00:15) + if dep_td.total_seconds() >= 3600 * 23: + dep_date -= datetime.timedelta(days=1) + + # Recalculate the timedelta + return BubaAnimation.countdown(datetime.datetime.combine(dep_date, dep_time)) @staticmethod def short_station(station: str) -> str: @@ -88,8 +95,6 @@ class DBF(BubaAnimation): train = "EN" if train.startswith("ME"): train = "ME" - if train.startswith("NB"): - train = "NB" if train.startswith("NJ"): train = "NJ" if train.startswith("RB"): @@ -99,10 +104,31 @@ class DBF(BubaAnimation): train = train.replace(" ", "") return train - def show(self): - self.rows = [[ - self.short_train(train['train']), - self.short_station(train['destination']), - self.countdown(train['actualDeparture'], train['isCancelled']), - ] for train in self.trains[:self.count]] - self.show_pages() + def run(self): + all = self.trains[:self.count] + all_len = int((len(all) + 1) / 3) + + if len(self.trains) == 0: + sleep(5) + + for page, trains in enumerate(self.chunk(all, 3)): + if all_len == 1: + title = self.station + else: + title = f"{self.station} ({page + 1}/{all_len})" + self.buba.text(page=0, row=0, col_start=0, col_end=92, text=title, align=BubaCmd.ALIGN_LEFT) + for i, train in enumerate(trains): + if train['isCancelled']: + when = "--" + else: + when = self.countdown(train['actualDeparture']) + self.buba.text(page=0, row=i + 1, col_start=0, col_end=11, text=self.short_train(train['train'])) + self.buba.text(page=0, row=i + 1, col_start=12, col_end=104, + text=self.short_station(train['destination'])) + self.buba.text(page=0, row=i + 1, col_start=105, col_end=119, + text=when, align=BubaCmd.ALIGN_RIGHT) + self.buba.set_page(0) + for i in range(5): + self.buba.text(page=0, row=0, col_start=93, col_end=119, text=datetime.datetime.now().strftime("%H:%M"), + align=BubaCmd.ALIGN_RIGHT) + sleep(2) diff --git a/buba/animations/homeassistant.py b/buba/animations/homeassistant.py deleted file mode 100644 index f1a1acb..0000000 --- a/buba/animations/homeassistant.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -Data from club-assistant.hamburg.ccc.de -""" -import json -from threading import Thread -from time import sleep - -import requests - -from buba.bubaanimation import BubaAnimation - -default_endpoints = [ - { - "endpoint": "sensor.total_measured_power", - "name": "Current Power", - }, - { - "endpoint": "sensor.hauptraum_scd41_co2", - "name": "Hauptraum CO2", - }, - { - "endpoint": "sensor.werkstatt_scd41_co2", - "name": "Werkstatt CO2", - }, - { - "endpoint": "sensor.temperatur_und_feuchtigkeitssensor_hauptraum_humidity", - "name": "Hauptraum Luftfeuchte", - }, - { - "endpoint": "sensor.temperatur_und_feuchtigkeitssensor_lotschlauch_humidity", - "name": "Lötschlauch Luftfeuchte", - } -] - -class HomeAssistant(BubaAnimation): - def __init__(self, buba, url, title, token, endpoints=None): - if endpoints is None: - endpoints = default_endpoints - super().__init__(buba) - self.url = url - self.title = title - self.token = token - self.endpoints = endpoints - - def update(self): - rows = [] - for e in self.endpoints: - res = requests.get(self.url + "/api/states/" + e["endpoint"], headers={'Authorization': 'Bearer ' + self.token}) - res.raise_for_status() - js = json.loads(res.text) - rows.append([e["name"], f"{js['state']}{js['attributes']['unit_of_measurement']}"]) - self.rows = rows diff --git a/buba/animations/icalevents.py b/buba/animations/icalevents.py index 4a74e56..a51f16f 100644 --- a/buba/animations/icalevents.py +++ b/buba/animations/icalevents.py @@ -1,34 +1,48 @@ import os from datetime import timedelta, datetime +from threading import Thread +from time import sleep import icalevents.icalevents from pytz import timezone -from buba.bubaanimation import BubaAnimation +from buba.bubaanimator import BubaAnimation +from buba.bubacmd import BubaCmd class IcalEvents(BubaAnimation): - def __init__(self, buba, url, title, range=timedelta(weeks=2)): + def __init__(self, buba, url, title): super().__init__(buba) self.url = url self.title = title - self.range = range + self.events = [] + Thread(target=self.update, daemon=True).start() def __repr__(self): return f"<{type(self).__name__}, {self.url}>" def update(self): - """ - Fetch all events from now to the end of the range. - Filter out events that have ended. Apparently, the icalecvents list can contain entries - that are slightly in the past (by an hours or two). Only use entries that have not reached - their end date/time yet. - :return: - """ tz = timezone(os.getenv("TZ", "Europe/Berlin")) - now = datetime.now(tz) - events = icalevents.icalevents.events(self.url, tzinfo=tz, sort=True, end=now + self.range) + events = icalevents.icalevents.events(self.url, tzinfo=tz, sort=True, end=datetime.now(tz) + timedelta(days=14)) for event in events: event.start = event.start.astimezone(tz) - events = filter(lambda e: e.end > now, events) - self.rows = [[e.summary, self.countdown(e.start)] for e in events] + self.events = events + sleep(600) + + def run(self): + for (page, events) in enumerate(self.chunk(self.events, 3)): + if len(self.events) > 3: + self.buba.text(page=0, row=0, col_start=0, col_end=119, + text=f"{self.title} ({page + 1}/{int((len(self.events) + 2) / 3)})", + align=BubaCmd.ALIGN_LEFT) + else: + self.buba.text(page=0, row=0, col_start=0, col_end=119, text=self.title, align=BubaCmd.ALIGN_LEFT) + for i in range(3): + if i >= len(events): + self.buba.text(page=0, row=i + 1, col_start=0, col_end=119, text="") + else: + event = events[i] + self.buba.text(page=0, row=i + 1, col_start=0, col_end=100, text=self.ellipsis(event.summary, 25)) + self.buba.text(page=0, row=i + 1, col_start=101, col_end=119, + text=self.countdown(event.start), align=BubaCmd.ALIGN_RIGHT) + sleep(10) diff --git a/buba/animations/snake.py b/buba/animations/snake.py index 6eb52e2..404cad6 100644 --- a/buba/animations/snake.py +++ b/buba/animations/snake.py @@ -1,8 +1,7 @@ import random -from datetime import datetime, timedelta from time import sleep -from buba.bubaanimation import BubaAnimation +from buba.bubaanimator import BubaAnimation class SnakeAnimation(BubaAnimation): @@ -35,7 +34,7 @@ class SnakeAnimation(BubaAnimation): random.seed() - def show(self): + def run(self): self.grid = [list([0] * self.width) for i in range(self.height)] self.prev_grid = [list([0] * self.width) for i in range(self.height)] x = random.randrange(self.width) @@ -47,7 +46,6 @@ class SnakeAnimation(BubaAnimation): self.body = [(x, y)] self.render() iterations = 0 - last_update = datetime.now() while True: if self.is_blocked(x, y, d): end = True @@ -73,11 +71,8 @@ class SnakeAnimation(BubaAnimation): self.grid[ty][tx] = 0 (tx, ty) = self.body[0] self.grid[ty][tx] = 11 - if datetime.now() - last_update > timedelta(seconds=1): - last_update = datetime.now() - self.render() - else: - sleep(.5) + self.render() + sleep(1) sleep(5) @staticmethod diff --git a/buba/animations/spaceapi.py b/buba/animations/spaceapi.py deleted file mode 100644 index e98a59a..0000000 --- a/buba/animations/spaceapi.py +++ /dev/null @@ -1,54 +0,0 @@ -import json -import logging -from datetime import datetime -from threading import Thread -from time import sleep - -import requests - -from buba.bubaanimation import BubaAnimation -from buba.bubacmd import BubaCmd - - -class Spaceapi(BubaAnimation): - def __init__(self, buba, url, title): - super().__init__(buba) - self.url = url - self.title = title - - def update(self): - res = requests.get(self.url) - data = json.loads(res.text) - - open = "open" if data["state"]["open"] else "closed" - since = datetime.fromtimestamp(data["state"]["lastchange"]).astimezone() - temp = int(data["sensors"]["temperature"][0]["value"]) - hum = int(data["sensors"]["humidity"][0]["value"]) - printers = {} - for p in data["sensors"]["ext_3d_printer_busy_state"]: - printers[p["name"]] = { - "busy": p["value"] != 0, - } - for p in data["sensors"]["ext_3d_printer_minutes_remaining"]: - printers[p["name"]]["remaining"] = p["value"] - printstatus = [] - for n, p in sorted(printers.items()): - if p["busy"]: - printstatus.append(f"{n} {p['remaining']}m left") - else: - printstatus.append(f"{n} idle") - self.rows = [ - [f"CCCHH {open} {self.humanize(since)}"], - [f"Outside: {temp}°C at {hum}% rel.hum."], - [", ".join(printstatus)] - ] - - def humanize(self, dt): - td = dt - datetime.now().astimezone() - if td.total_seconds() > -60: - return "just now" - if td.total_seconds() > -3600: - return f"for {-int(td.total_seconds()/60)} minutes" - if td.total_seconds() >- 86400: - return f"for {-int(td.total_seconds()/3600)} hours" - return dt.strftime("since %y-%m-%d %H:%M") diff --git a/buba/animations/time.py b/buba/animations/time.py index 78ef2f1..f681171 100644 --- a/buba/animations/time.py +++ b/buba/animations/time.py @@ -1,19 +1,22 @@ from datetime import datetime from time import sleep -from buba.bubaanimation import BubaAnimation +from buba.bubaanimator import BubaAnimation from buba.bubacmd import BubaCmd class BubaTime(BubaAnimation): - def __init__(self, buba: BubaCmd, ip:str): + def __init__(self, buba: BubaCmd): super().__init__(buba) - self.ip = ip - def show(self): - self.buba.text(page=0, row=0, col_start=0, col_end=119, text="Chaos Computer Club", align=BubaCmd.ALIGN_CENTER) - self.buba.text(page=0, row=1, col_start=0, col_end=119, text="Hansestadt Hamburg", align=BubaCmd.ALIGN_CENTER) - self.buba.text(page=0, row=2, col_start=0, col_end=119, text=f"{self.ip}", align=BubaCmd.ALIGN_CENTER) - self.buba.text(page=0, row=3, col_start=0, col_end=119, text=datetime.now().strftime("%Y-%m-%d %H:%M"), align=BubaCmd.ALIGN_CENTER) + def run(self): + self.buba.text(page=0, row=0, col_start=0, col_end=92, text="Bus-Bahn-Anzeige") + self.buba.simple_text(page=0, row=1, col=0, text="Chaos Computer Club") + self.buba.simple_text(page=0, row=2, col=0, text="Hansestadt Hamburg") + self.buba.simple_text(page=0, row=3, col=0, text="Hello, world!") + self.buba.set_page(0) - sleep(10) + for i in range(3): + self.buba.text(page=0, row=0, col_start=93, col_end=119, text=datetime.now().strftime("%H:%M"), + align=BubaCmd.ALIGN_RIGHT) + sleep(2) diff --git a/buba/animations/webqueue.py b/buba/animations/webqueue.py deleted file mode 100644 index 66bbde5..0000000 --- a/buba/animations/webqueue.py +++ /dev/null @@ -1,48 +0,0 @@ -from datetime import timedelta -from queue import Queue, Empty -from time import sleep - -from buba.bubaanimation import BubaAnimation -from buba.bubaanimator import LineLayoutColumn -from buba.bubacmd import BubaCmd - - -class WebQueue(BubaAnimation): - """ - - """ - def __init__(self, buba, maxlen:int=3, maxrows:int=6): - """ - A queue of pages to show. If - :param buba: - :param maxlen: - """ - super().__init__(buba) - self.title = "(waiting for input)" - self.queue = Queue() - self.maxlen = maxlen - self.maxrows = maxrows - - def show(self) -> None: - try: - (self.title, self.rows) = self.queue.get(block=False) - self.layout = self.default_layout - self.show_pages() - except Empty: - self.buba.text(page=0, row=0, col_start=0, col_end=119, text="\u0010\u0010 Your Data Here \u0011\u0011", align=BubaCmd.ALIGN_CENTER) - self.buba.text(page=0, row=1, col_start=0, col_end=119, text="Submit your own data to", align=BubaCmd.ALIGN_CENTER) - self.buba.text(page=0, row=2, col_start=0, col_end=119, text="this display! Go to", align=BubaCmd.ALIGN_CENTER) - self.buba.text(page=0, row=3, col_start=0, col_end=119, text="https://buba.ccchh.net", align=BubaCmd.ALIGN_CENTER) - sleep(7) - - def add(self, title, rows) -> bool: - """ - Add an entry to the queue. Returns False if the maximum number of entries has been reached. - :param title: - :param rows: - :return: - """ - if self.queue.qsize() >= self.maxlen: - return False - self.queue.put((title, rows)) - return True \ No newline at end of file diff --git a/buba/appconfig.py b/buba/appconfig.py index 89befaf..9124a9d 100644 --- a/buba/appconfig.py +++ b/buba/appconfig.py @@ -16,15 +16,8 @@ class AppConfig: self.url = getenv('BUBA_URL', 'http://localhost:3000') (self.listen_host, self.listen_port) = getenv('BUBA_LISTEN', '127.0.0.1:3000').split(':') self.serial = getenv('BUBA_SERIAL', '/dev/ttyUSB0') - self.clubassistant_token = getenv('BUBA_CLUBASSISTANT_TOKEN', None) if self.debug is not None and self.debug.lower not in ('0', 'f', 'false'): self.debug = True else: self.debug = False - - import socket - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.connect(("8.8.8.8", 80)) - self.ip = s.getsockname()[0] - s.close() diff --git a/buba/bubaanimation.py b/buba/bubaanimation.py deleted file mode 100644 index f601c39..0000000 --- a/buba/bubaanimation.py +++ /dev/null @@ -1,180 +0,0 @@ -import logging -from datetime import timedelta, datetime -from itertools import islice -from threading import Thread -from time import sleep - -from buba.bubaanimator import LineLayoutColumn, WEEKDAYS_DE -from buba.bubacmd import BubaCmd - - -class BubaAnimation: - default_layout = [ - LineLayoutColumn(90, BubaCmd.ALIGN_LEFT), - LineLayoutColumn(30, BubaCmd.ALIGN_RIGHT), - ] - - def __init__(self, buba: BubaCmd): - self.log = logging.getLogger(type(self).__name__) - self.buba = buba - self.update_interval = timedelta(minutes=1) - self.page_interval = timedelta(seconds=7) - self.title = None - self.layout = self.default_layout - self.rows = [] - self.thread = Thread(target=self.update_rows, daemon=True) - self.thread.start() - pass - - def __repr__(self): - return f"<{type(self).__name__}>" - - def update(self) -> None: - """ - Do everything necessary to produce the information to be displayed. - :return: - """ - - def update_rows(self) -> None: - """ - Update loop. This will be run continuously in a thread to call update(). - :return: - """ - sleep(1) # give the code a bit of time to configure stuff - while True: - try: - self.update() - except Exception as e: - self.log.warning(f"unable to update: {e}") - sleep(self.update_interval.total_seconds()) - - def show(self) -> None: - """ - Write information to the display. - You should also implement any waiting time for people to be able to take the information in. - """ - self.show_pages() - - @staticmethod - def chunk(it, size): - """ - Return list in groups of size. - :param it: list - :param size: chunk size - :return: list of chunks - """ - it = iter(it) - return iter(lambda: tuple(islice(it, size)), ()) - - @staticmethod - def humanize_delta(dt, now_delta, day_delta): - """ - Produce a short text representing a target date and time. - :param dt: the target date and time - :param now_delta: time delta of the target time to now - :param day_delta: delta rounded to the full day - :return: - """ - if now_delta < timedelta(seconds=60): - return "jetzt" - if now_delta < timedelta(minutes=90): - return f"{int(now_delta.seconds / 60)}m" - if day_delta < timedelta(hours=24): - return f"{int((now_delta.seconds + 3599) / 3600)}h" - if day_delta < timedelta(days=7): - # return dt.strftime("%a") # weekday - return WEEKDAYS_DE[dt.weekday()] - return dt.strftime("%d.%m.") - - @staticmethod - def countdown(dt: datetime): - """ - Compute a human-readable time specification until the target event starts. The day starts at 04:00. - :param dt: datetime timezone-aware datetime - :return: - """ - now = datetime.now(dt.tzinfo) - from_day_start = now.replace(hour=4, minute=0, second=0, microsecond=0) - now_delta = dt - now - day_delta = dt - from_day_start - h = BubaAnimation.humanize_delta(dt, now_delta, day_delta) - return h - - @staticmethod - def ellipsis(text, max=28): - """ - If the text is longer that max, shorten it and add ellipsis. - :param text: to be shortened - :param max: max length - :return: shortened text - """ - if len(text) > max: - text = text[:max - 2] + "..." # we can get away with just 2, since the periods are very narrow - return text - - def _write_row(self, nrow: int, row:list[str], layout: list[LineLayoutColumn]) -> None: - """ - Write one row to the display. - :param nrow: row number to write - :param row: contents of the row - :param layout: layout to use - :return: - """ - col = 0 - for i, ll in enumerate(layout): - t = row[i] if len(row) > i else "" - if ll.width > 24: - t = self.ellipsis(t, int(ll.width / 3.7)) - self.buba.text(page=0, row=nrow, col_start=col, col_end=col + ll.width - 1, text=t, align=ll.align) - col += ll.width - - def write_title(self, row: list[str], layout: list[LineLayoutColumn] = None) -> None: - """ - Write the title row. If row has one element, fill the entire row with that; if it has two, the second one - is right-aligned and shows a page number or the current time. - :param row: the title info - :param layout: the layout to use - :return: - """ - if len(row) == 1: - self._write_row(0, row, [LineLayoutColumn(120, BubaCmd.ALIGN_LEFT)]) - else: - self._write_row(0, row, [ - LineLayoutColumn(100, BubaCmd.ALIGN_LEFT), - LineLayoutColumn(20, BubaCmd.ALIGN_RIGHT), - ]) - - def write_row(self, nrow: int, row: list[str]) -> None: - """ - Write one row to the display. Use the layout specification to format the row. - :param nrow: row number to be written - :param row: contents of the row - :param layout: layout to use - :return: - """ - if len(row) == 1: - # if the row only has a single part, simply show that over the entire width - self._write_row(nrow, row, [LineLayoutColumn(120, BubaCmd.ALIGN_LEFT)]) - else: - self._write_row(nrow, row, self.layout) - - def show_pages(self) -> None: - """ - Show rows on the display. Paginate the rows if there are more rows than the display can show. - :return: - """ - pages = list(self.chunk(self.rows, 3)) - for n, page in enumerate(pages): - if len(pages) <= 1: - self.write_title([self.title]) - else: - self.write_title([self.title, f"({n + 1}/{len(pages)})"]) - for i in range(3): - if i >= len(page): - self.buba.text(page=0, row=i + 1, col_start=0, col_end=119, text="") - else: - p = page[i] - if isinstance(p, str): - p = [p, ] - self.write_row(i + 1, p) - sleep(self.page_interval.total_seconds()) diff --git a/buba/bubaanimator.py b/buba/bubaanimator.py index 91b2e2b..b34bef1 100644 --- a/buba/bubaanimator.py +++ b/buba/bubaanimator.py @@ -1,29 +1,67 @@ import logging -import random -from dataclasses import dataclass +from datetime import datetime, timedelta +from itertools import islice from threading import Thread from time import sleep from buba.bubacmd import BubaCmd -LOG = logging.getLogger(__name__) -WEEKDAYS_DE = [ - "Mo", - "Di", - "Mi", - "Do", - "Fr", - "Sa", - "So" -] +class BubaAnimation: + def __init__(self, buba: BubaCmd): + self.log = logging.getLogger(type(self).__name__) + self.buba = buba + pass + def __repr__(self): + return f"<{type(self).__name__}>" -# class layout with width, alignment; list of layouts -@dataclass -class LineLayoutColumn: - width: int - align: int + def run(self): + raise Exception("Your class must implement a run() method") + + @staticmethod + def chunk(it, size): + """ + Return list in groups of size. + :param it: list + :param size: chunk size + :return: list of chunks + """ + it = iter(it) + return iter(lambda: tuple(islice(it, size)), ()) + + @staticmethod + def countdown(dt: datetime): + """ + Compute a human-readable time specification until the target event starts. The day starts at 04:00. + :param dt: datetime timezone-aware datetime + :return: + """ + now = datetime.now(dt.tzinfo) + from_day_start = now.replace(hour=4, minute=0, second=0, microsecond=0) + now_delta = dt - now + day_delta = dt - from_day_start + if now_delta < timedelta(seconds=60): + return "now" + if now_delta < timedelta(minutes=30): + return f"{int(now_delta.seconds / 60)}m" + if day_delta < timedelta(hours=24): + return f"{int((now_delta.seconds + 3599) / 3600)}h" + if day_delta < timedelta(days=7): + return dt.strftime("%a") # weekday + return dt.strftime("%d.%m.") + + @staticmethod + def ellipsis(text, max=28): + """ + If the text is longer that max, shorten it and add ellipsis. + :param text: to be shortened + :param max: max length + :return: shortened text + """ + if len(text) > max: + text = text[:max - 2] + "..." # we can get away with just 2, since the periods are very narrow + return text class BubaAnimator: @@ -34,23 +72,14 @@ class BubaAnimator: Thread(target=self.run, daemon=True).start() def run(self): - for nrow in range(4): - self.buba.text(page=0, row=nrow, col_start=0, col_end=119, text="") - self.buba.text(page=0, row=1, col_start=0, col_end=119, text="...booting...", align=BubaCmd.ALIGN_CENTER) - sleep(5) - while True: if len(self.animations) == 0: self.log.debug("No animations, sleeping...") sleep(2) else: - for a in random.sample(self.animations, len(self.animations)): + for a in self.animations: self.log.debug(f"Starting animation: {a}") - try: - a.show() - except Exception as e: - self.log.warning(f"Exception while showing animation: {e}") - sleep(2) + a.run() - def add(self, animation): - self.animations.append(animation) + def add(self, animation, *args, **kwargs): + self.animations.append(animation(self.buba, *args, **kwargs)) diff --git a/buba/bubacmd.py b/buba/bubacmd.py index 67c35d5..030e22e 100644 --- a/buba/bubacmd.py +++ b/buba/bubacmd.py @@ -64,7 +64,6 @@ class BubaCmd: :param align: alignment options, see MIS1TextDisplay.ALIGN_* :return: """ - text = text.replace("\u00ad", "") # remove soft hyphen text = text.encode("CP437", "replace").decode("CP437") # force text to be CP437 compliant if self.display is not None: self.display.text(page, row, col_start, col_end, text, align) diff --git a/buba/static/main.css b/buba/static/main.css index 458f158..19924ae 100644 --- a/buba/static/main.css +++ b/buba/static/main.css @@ -29,27 +29,4 @@ svg.geavision__row { .gvson { fill: #4f0; -} - -.info-form__row { - display: flex; - padding: 2px 0; -} - -.info-form__field { - display: flex; - flex-direction: column; - padding: 0 1em 0 0; -} - -.info-form__field * { - display: block; -} - -.info-form__field label { - font-size: 0.9em; -} - -.info-form__field input { - font-size: 1.1em; } \ No newline at end of file diff --git a/buba/static/main.js b/buba/static/main.js index b9966ab..7edc355 100644 --- a/buba/static/main.js +++ b/buba/static/main.js @@ -11,7 +11,7 @@ if (container) { if (m.cmd === undefined) { console.log("undefined command", m) } - switch (m.cmd) { + switch(m.cmd) { case "set_pages": display.set_pages(m.pages); break; @@ -32,44 +32,5 @@ if (container) { ws.close(); }); } - connect(); } - -let info_send = document.getElementById("info-send"); -if (info_send) { - document.getElementById("info-form__form").addEventListener("submit", (e) => { - e.preventDefault(); - e.stopPropagation(); - }) - info_send.addEventListener("click", (e) => { - let info = { - "title": document.getElementById("info-form__title").value, - "rows": [] - } - for (let e of document.querySelectorAll(".info-form__row-data")) { - let t = e.querySelector(".info-row__field-text").value; - let v = e.querySelector(".info-row__field-value").value; - info.rows.push([t, v]); - } - fetch("/user-entry", { - method: "POST", - body: JSON.stringify(info), - headers: { - "Content-Type": "application/json" - } - }).then(res => { - if (res.status === 200) { - alert("Your entry has been sent!") - } else { - return res.json() - } - }).then(json => { - alert("Your entry could not be accepted: " + json.error) - }).catch((err) => { - console.log(err); - }) - e.preventDefault(); - e.stopPropagation(); - }) -} \ No newline at end of file diff --git a/buba/templates/base.html.j2 b/buba/templates/base.html.j2 index 5d0523d..28bb51c 100644 --- a/buba/templates/base.html.j2 +++ b/buba/templates/base.html.j2 @@ -8,6 +8,7 @@ +

{{ self.page_title() }}

{% block page_body %}{% endblock %} \ No newline at end of file diff --git a/buba/templates/home.html.j2 b/buba/templates/home.html.j2 index b729ab6..7f878a6 100644 --- a/buba/templates/home.html.j2 +++ b/buba/templates/home.html.j2 @@ -2,43 +2,4 @@ {% block page_title %}CCCHH Buba{% endblock %} {% block page_body %}
...
-

Submit Your Own Info

-
-
-
-
- - -
-
- {% for r in user_forms %} -
-
-
-
-
-
-
- -
-
- {% endfor %} -
-
- -
-
-
-

Submitting info via REST

-

You can also submit your info via a PUT to https://buba.ccchh.net/user-entry, with a JSON body like this:

-
-{
-    "title": "My great info",
-    "rows": [
-        ["line 1 info", "123"],
-        ["line 2 info", "23:42"]
-    ]
-}
-    
-
{% endblock %}