Add station departure display

This commit is contained in:
Stefan Bethke 2025-06-02 17:45:43 +02:00
commit 56695e7874
13 changed files with 457 additions and 88 deletions

View file

@ -1,76 +0,0 @@
import logging
from datetime import datetime
from threading import Thread
from time import sleep
from buba.BubaCmd import BubaCmd
class BubaAnimation:
def __init__(self, buba: BubaCmd):
self.buba = buba
pass
def run(self):
pass
def __repr__(self):
return f"<{type(self).__name__}>"
class BubaTime(BubaAnimation):
def __init__(self, buba: BubaCmd):
super().__init__(buba)
def run(self):
self.buba.simple_text(page=0, row=0, col=0, text="Buba")
self.buba.simple_text(page=0, row=1, col=0, text="Choas 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)
for i in range(5):
self.buba.simple_text(page=0, row=0, col=93, text=datetime.now().strftime("%H:%M:%S"))
sleep(1)
class BubaCharset(BubaAnimation):
def __init__(self, buba: BubaCmd, single=None):
super().__init__(buba)
self.charset = bytes(range(256)).decode("CP437")
self.single = single
def run(self):
if self.single is not None:
while True:
self.render(self.single)
sleep(1)
else:
for page in range(16):
self.render(page)
sleep(5)
def render(self, page):
for row in range(4):
start = ((page + row) * 16) % 256
self.buba.simple_text(page=0, row=row, col=0, text=f"${start:02x}: {self.charset[start:start + 16]}")
class BubaAnimator:
def __init__(self, buba: BubaCmd):
self.log = logging.getLogger(__name__)
self.buba = buba
self.animations = []
Thread(target=self.run, daemon=True).start()
def run(self):
while True:
if len(self.animations) == 0:
a = BubaTime(self.buba)
a.run()
else:
for a in self.animations:
self.log.debug(f"Starting animation: {a}")
a.run()
def add(self, animation, *args, **kwargs):
self.animations.append(animation(self.buba, *args, **kwargs))

View file

@ -5,9 +5,12 @@ from bottle_log import LoggingPlugin
from bottle_websocket import websocket, GeventWebSocketServer from bottle_websocket import websocket, GeventWebSocketServer
from geventwebsocket.websocket import WebSocket from geventwebsocket.websocket import WebSocket
from buba.AppConfig import AppConfig from buba.appconfig import AppConfig
from buba.BubaAnimator import BubaAnimator, BubaTime, BubaCharset from buba.bubaanimator import BubaAnimator
from buba.BubaCmd import BubaCmd from buba.animations.time import BubaTime
from buba.bubacmd import BubaCmd
from buba.animations.dbf import DBFAnimation
from buba.websocketcomm import WebSocketClients from buba.websocketcomm import WebSocketClients
config = AppConfig() config = AppConfig()
@ -25,10 +28,10 @@ TEMPLATE_PATH.insert(0, config.templatepath)
websocket_clients = WebSocketClients() websocket_clients = WebSocketClients()
buba = BubaCmd(config.serial, websocket_clients.send) buba = BubaCmd(config.serial, websocket_clients.send)
animator = BubaAnimator(buba) animator = BubaAnimator(buba)
animator.add(BubaCharset) #, single=12) # animator.add(BubaCharset) #, single=12)
animator.add(BubaTime) animator.add(BubaTime)
# bottle_helpers = BottleHelpers(auth, group=config.requires_group, allowed=config.allowed) animator.add(DBFAnimation, ds100="AHST", station="Holstenstraße")
# update_poller = UpdatePoller(websocket_clients, ccujack, 1 if config.debug else 0.1) animator.add(DBFAnimation, ds100="AHS", station="Altona", count=9)
@app.route("/static/<filepath>") @app.route("/static/<filepath>")

View file

View file

@ -0,0 +1,26 @@
from time import sleep
from buba.bubaanimator import BubaAnimation
from buba.bubacmd import BubaCmd
class BubaCharset(BubaAnimation):
def __init__(self, buba: BubaCmd, single=None):
super().__init__(buba)
self.charset = bytes(range(256)).decode("CP437")
self.single = single
def run(self):
if self.single is not None:
while True:
self.render(self.single)
sleep(1)
else:
for page in range(16):
self.render(page)
sleep(5)
def render(self, page):
for row in range(4):
start = ((page + row) * 16) % 256
self.buba.simple_text(page=0, row=row, col=0, text=f"${start:02x}: {self.charset[start:start + 16]}")

123
buba/animations/dbf.py Normal file
View file

@ -0,0 +1,123 @@
"""
Pull departure info from https://trains.xatlabs.com and display.
See also https://github.com/derf/db-fakedisplay/blob/main/README.md
"""
import datetime
from itertools import islice
from threading import Thread
from time import sleep
from deutschebahn.db_infoscreen import DBInfoscreen
from buba.bubaanimator import BubaAnimation
from buba.bubacmd import BubaCmd
class DBFAnimation(BubaAnimation):
def __init__(self, buba: BubaCmd, ds100="AHST", station="Holstenstraße", count=3):
super().__init__(buba)
self.ds100 = ds100
self.station = station
self.trains = []
self.count = count
Thread(target=self.update, daemon=True).start()
def fetch(self):
dbi = DBInfoscreen("trains.xatlabs.com")
trains = dbi.calc_real_times(dbi.get_trains(self.ds100)) # Station
trains = [t for t in trains] # if t['platform'] == "1"] # platform gleis
trains.sort(key=dbi.time_sort)
self.trains = trains
def update(self):
self.fetch()
sleep(60)
@staticmethod
def countdown(train):
now = datetime.datetime.now()
try:
dep_time = datetime.datetime.strptime(train['actualDeparture'], "%H:%M").time()
except TypeError as e:
return -1
# First, assume all departure times are on the current day
dep_date = now.date()
# 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
dep_td = datetime.datetime.combine(dep_date, dep_time) - now
return round(dep_td.total_seconds() / 60)
@staticmethod
def short_station(station: str) -> str:
station = station.strip()
if station.startswith("Hamburg-") or station.startswith("Hamburg "):
station = station[8:]
if station.endswith("(S)"):
station = station[:-3]
if station.endswith("(S-Bahn)"):
station = station[:-8]
if station == "Hbf":
station = "Hauptbahnhof"
return station
@staticmethod
def short_train(train: str) -> str:
if train.startswith("ICE"):
train = "ICE"
if train.startswith("ME"):
train = "ME"
if train.startswith("RE"):
train = "RE"
train = train.replace(" ", "")
return train
@staticmethod
def chunk(it, size):
it = iter(it)
return iter(lambda: tuple(islice(it, size)), ())
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=120, text=title, align=BubaCmd.ALIGN_LEFT)
for i, train in enumerate(trains):
if train['isCancelled']:
when = "--"
else:
when = self.countdown(train)
if when < 0:
when = 0
self.buba.text(page=0, row=i + 1, col_start=0, col_end=120, text=self.short_train(train['train']))
self.buba.text(page=0, row=i + 1, col_start=12, col_end=76, text=self.short_station(train['destination']))
self.buba.text(page=0, row=i + 1, col_start=105, col_end=120,
text=str(when), align=BubaCmd.ALIGN_RIGHT)
self.buba.set_page(0)
for i in range(10):
self.buba.text(page=0, row=0, col_start=93, col_end=120, text=datetime.datetime.now().strftime("%H:%M:%S"), align=BubaCmd.ALIGN_RIGHT)
sleep(1)

21
buba/animations/time.py Normal file
View file

@ -0,0 +1,21 @@
from datetime import datetime
from time import sleep
from buba.bubaanimator import BubaAnimation
from buba.bubacmd import BubaCmd
class BubaTime(BubaAnimation):
def __init__(self, buba: BubaCmd):
super().__init__(buba)
def run(self):
self.buba.simple_text(page=0, row=0, col=0, 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)
for i in range(5):
self.buba.text(page=0, row=0, col_start=93, col_end=120, text=datetime.now().strftime("%H:%M:%S"), align=BubaCmd.ALIGN_RIGHT)
sleep(1)

38
buba/bubaanimator.py Normal file
View file

@ -0,0 +1,38 @@
import logging
from threading import Thread
from time import sleep
from buba.bubacmd import BubaCmd
class BubaAnimation:
def __init__(self, buba: BubaCmd):
self.buba = buba
pass
def run(self):
pass
def __repr__(self):
return f"<{type(self).__name__}>"
class BubaAnimator:
def __init__(self, buba: BubaCmd):
self.log = logging.getLogger(__name__)
self.buba = buba
self.animations = []
Thread(target=self.run, daemon=True).start()
def run(self):
while True:
if len(self.animations) == 0:
self.log.debug("No animations, sleeping...")
sleep(2)
else:
for a in self.animations:
self.log.debug(f"Starting animation: {a}")
a.run()
def add(self, animation, *args, **kwargs):
self.animations.append(animation(self.buba, *args, **kwargs))

View file

@ -9,6 +9,11 @@ from pyfis.aegmis import MIS1TextDisplay
class BubaCmd: class BubaCmd:
ALIGN_LEFT = 0x00
ALIGN_RIGHT = 0x01
ALIGN_CENTER = 0x02
ALIGN_SCROLL = 0x03 # apparently not supported
def __init__(self, serial: str, send: Callable): def __init__(self, serial: str, send: Callable):
self.log = logging.getLogger(__name__) self.log = logging.getLogger(__name__)
self.serial = serial self.serial = serial

View file

@ -119,15 +119,44 @@ export default class {
return col; return col;
} }
text_length(text) {
let l = 0
for (let c of text) {
let f = this.font[c];
if (f === undefined)
continue;
l += f.length;
}
return l
}
col_start_for_alignment(col_start, col_end, text, align) {
let l = this.text_length(text)
switch (align) {
case 0: // ALIGN_LEFT
return col_start;
case 1: // ALIGN_RIGHT
return col_end - l;
case 2:
return Math.floor((col_end - col_start - l) / 2);
}
}
set_pages(pages) { set_pages(pages) {
console.log("set pages", pages); console.log("set pages", pages);
// ignore for now // ignore for now
} }
text(page, row, col_start, col_end, text, align) {
let s = this.col_start_for_alignment(col_start, col_end, text, align);
if (s < 0)
console.log("Out of alignment")
this.eraseCols(row, col_start, col_end);
this.applyText(row, s, text);
}
simple_text(page, row, col, text, align) { simple_text(page, row, col, text, align) {
// console.log("simple text", page, row, col, text, align); this.text(page, row, col, this.config.cols, text, align);
this.eraseCols(row, col, this.config.cols);
this.applyText(row, col, text);
} }
defineFont() { defineFont() {
@ -333,6 +362,8 @@ export default class {
// 0x20-0x2f // 0x20-0x2f
defs['\u0020'] = [ // space defs['\u0020'] = [ // space
'...... ...... ...... ......', '...... ...... ...... ......',
'...... ...... ...... ......',
'...... ...... ...... ......',
]; ];
defs['\u0021'] = [ // ! defs['\u0021'] = [ // !
'###### ###### ##.... ###...', '###### ###### ##.... ###...',

View file

@ -19,7 +19,7 @@ if (container) {
display.simple_text(m.page, m.row, m.col, m.text, m.align); display.simple_text(m.page, m.row, m.col, m.text, m.align);
break; break;
case "text": case "text":
console.log("text", m); display.text(m.page, m.row, m.col_start, m.col_end, m.text, m.align);
break; break;
} }
}); });

198
poetry.lock generated
View file

@ -42,6 +42,18 @@ files = [
bottle = "*" bottle = "*"
gevent-websocket = "*" gevent-websocket = "*"
[[package]]
name = "certifi"
version = "2025.4.26"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"},
{file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"},
]
[[package]] [[package]]
name = "cffi" name = "cffi"
version = "1.17.1" version = "1.17.1"
@ -123,6 +135,123 @@ files = [
[package.dependencies] [package.dependencies]
pycparser = "*" pycparser = "*"
[[package]]
name = "charset-normalizer"
version = "3.4.2"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"},
{file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"},
{file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"},
{file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"},
{file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"},
{file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"},
{file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"},
{file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"},
{file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"},
{file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"},
{file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"},
{file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"},
{file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"},
{file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"},
{file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"},
{file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"},
{file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"},
{file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"},
{file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"},
{file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"},
{file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"},
{file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"},
{file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"},
{file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"},
{file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"},
{file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"},
{file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"},
{file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"},
{file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"},
{file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"},
{file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"},
{file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"},
{file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"},
{file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"},
{file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"},
{file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"},
{file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"},
{file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"},
{file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"},
{file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"},
{file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"},
{file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"},
{file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"},
{file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"},
{file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"},
{file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"},
{file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"},
{file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"},
{file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"},
{file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"},
{file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"},
{file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"},
{file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"},
{file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"},
{file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"},
{file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"},
{file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"},
{file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"},
{file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"},
{file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"},
{file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"},
{file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"},
{file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"},
{file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"},
{file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"},
{file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"},
{file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"},
{file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"},
{file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"},
{file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"},
{file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"},
{file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"},
{file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"},
{file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"},
{file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"},
{file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"},
{file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"},
{file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"},
{file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"},
{file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"},
{file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"},
{file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"},
{file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"},
{file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"},
{file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"},
{file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"},
{file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"},
{file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"},
{file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"},
{file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"},
{file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"},
{file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"},
]
[[package]]
name = "deutschebahn"
version = "1.4.0"
description = "Collection of utilities related to German train operator Deutsche Bahn."
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "deutschebahn-1.4.0-py3-none-any.whl", hash = "sha256:0112e6030381c6916548c2d90fffca391672d060dd03f7f85471a82c23bef08f"},
{file = "deutschebahn-1.4.0.tar.gz", hash = "sha256:c84ae372cb15d50d0662dbad1cc9ca7803c63ed7fa4c3af2fe3bf1b98e734072"},
]
[package.dependencies]
requests = "*"
[[package]] [[package]]
name = "gevent" name = "gevent"
version = "25.5.1" version = "25.5.1"
@ -270,6 +399,21 @@ files = [
docs = ["Sphinx", "furo"] docs = ["Sphinx", "furo"]
test = ["objgraph", "psutil"] test = ["objgraph", "psutil"]
[[package]]
name = "idna"
version = "3.10"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
]
[package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]] [[package]]
name = "jinja2" name = "jinja2"
version = "3.1.6" version = "3.1.6"
@ -502,6 +646,40 @@ files = [
[package.extras] [package.extras]
cp2110 = ["hidapi"] cp2110 = ["hidapi"]
[[package]]
name = "pytz"
version = "2025.2"
description = "World timezone definitions, modern and historical"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"},
{file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"},
]
[[package]]
name = "requests"
version = "2.32.3"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
]
[package.dependencies]
certifi = ">=2017.4.17"
charset-normalizer = ">=2,<4"
idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<3"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]] [[package]]
name = "setuptools" name = "setuptools"
version = "80.9.0" version = "80.9.0"
@ -523,6 +701,24 @@ enabler = ["pytest-enabler (>=2.2)"]
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"]
[[package]]
name = "urllib3"
version = "2.4.0"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"},
{file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"},
]
[package.extras]
brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
[[package]] [[package]]
name = "zope-event" name = "zope-event"
version = "5.0" version = "5.0"
@ -600,4 +796,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"]
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.10,<4.0" python-versions = ">=3.10,<4.0"
content-hash = "d293c04556f191acb29cc8f27ed7f14ce989b097f93c1af920c443e26f439037" content-hash = "acf29f4ce322901a93ff75a105fbe8ba8ff40b944731aacd4f11cdb57f7d0481"

View file

@ -15,7 +15,9 @@ dependencies = [
"bottle-log (>=1.0.0,<2.0.0)", "bottle-log (>=1.0.0,<2.0.0)",
"gevent-websocket (>=0.10.1,<0.11.0)", "gevent-websocket (>=0.10.1,<0.11.0)",
"gevent (>=25.5.1,<26.0.0)", "gevent (>=25.5.1,<26.0.0)",
"jinja2 (>=3.1.6,<4.0.0)" "jinja2 (>=3.1.6,<4.0.0)",
"deutschebahn (>=1.4.0,<2.0.0)",
"pytz (>=2025.2,<2026.0)"
] ]