Implement transit time display

This commit is contained in:
Kenneth Allen 2025-05-15 21:38:26 +10:00
parent f8cc9f213c
commit b837fcac02
4 changed files with 53 additions and 49 deletions

View File

@ -1,15 +1,26 @@
from datetime import datetime from datetime import datetime, timedelta, timezone
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
import cache
import ptv
# Data
now = datetime.now(timezone.utc)
with cache.open() as c:
ptv_data = {
k: (v - now) // timedelta(minutes=1) if v else '-'
for k, v in ptv.get_departure_data(now, c).items()
}
BLACK = 0x000000 # 00 BGR BLACK = 0x000000 # 00 BGR
WHITE = 0xffffff # 01 WHITE = 0xffffff # 01
YELLOW = 0x00ffff # 10 YELLOW = 0x00ffff # 10
RED = 0x0000ff # 11 RED = 0x0000ff # 11
fontmm = ImageFont.truetype('NETWORKSANS-2019-REGULAR.TTF', 20) fontmm = ImageFont.truetype('assets/NETWORKSANS-2019-REGULAR.TTF', 20)
font24 = ImageFont.truetype('NETWORKSANS-2019-REGULAR.TTF', 24) font24 = ImageFont.truetype('assets/NETWORKSANS-2019-REGULAR.TTF', 24)
font42 = ImageFont.truetype('NETWORKSANS-2019-REGULAR.TTF', 42) font42 = ImageFont.truetype('assets/NETWORKSANS-2019-REGULAR.TTF', 42)
font64 = ImageFont.truetype('NETWORKSANS-2019-REGULAR.TTF', 64) font64 = ImageFont.truetype('assets/NETWORKSANS-2019-REGULAR.TTF', 64)
fonttime = ImageFont.truetype('NETWORKSANS-2019-REGULAR.TTF', 92) fonttime = ImageFont.truetype('assets/NETWORKSANS-2019-REGULAR.TTF', 92)
image = Image.new('RGB', (800, 480), WHITE) image = Image.new('RGB', (800, 480), WHITE)
draw = ImageDraw.Draw(image) draw = ImageDraw.Draw(image)
@ -19,7 +30,6 @@ with Image.open('assets/ui test 2.png').convert('RGBA') as bg:
image.paste(bg, (0, 0)) image.paste(bg, (0, 0))
# Time # Time
now = datetime.now()
draw.text((-3, 480-3), f'{now:%H:%M}', font=fonttime, anchor="ls", fill=WHITE) draw.text((-3, 480-3), f'{now:%H:%M}', font=fonttime, anchor="ls", fill=WHITE)
# Date # Date
@ -46,6 +56,16 @@ with Image.open(rainimgpath) as rainimg:
image.paste(rainimg, mask=rainimg) image.paste(rainimg, mask=rainimg)
draw.text((40, 55), f'mm', font=fontmm, anchor="mt") draw.text((40, 55), f'mm', font=fontmm, anchor="mt")
# PTV
draw.text((375, 120), f'{ptv_data["612 S"]}', font=font64, anchor="ms", fill=BLACK)
draw.text((500, 120), f'{ptv_data["612 N"]}', font=font64, anchor="ms", fill=BLACK)
draw.text((700, 120), f'{ptv_data["766 N"]}', font=font64, anchor="ms", fill=BLACK)
draw.text((375, 270), f'{ptv_data["Union W Express"]}', font=font64, anchor="ms", fill=BLACK)
draw.text((500, 270), f'{ptv_data["Union W"]}', font=font64, anchor="ms", fill=BLACK)
draw.text((700, 270), f'{ptv_data["Union E"]}', font=font64, anchor="ms", fill=BLACK)
draw.text((500, 420), f'{ptv_data["109 W"]}', font=font64, anchor="ms", fill=BLACK)
draw.text((700, 420), f'{ptv_data["70 W"]}', font=font64, anchor="ms", fill=BLACK)
image.save('test_before_palette.png') image.save('test_before_palette.png')
# Dither into eink palette # Dither into eink palette

View File

@ -6,12 +6,12 @@ class Cache:
def __init__(self, db): def __init__(self, db):
self.db = db self.db = db
def get(self, key, timeout, f): def get(self, key, ttl, f):
now = datetime.datetime.now(datetime.timezone.utc) now = datetime.datetime.now(datetime.timezone.utc)
earliest_acceptable = now - timeout earliest_acceptable = now - ttl
match self.db.execute( match self.db.execute(
'SELECT value FROM cache WHERE key = ? AND timestamp >= ?', 'SELECT value FROM cache WHERE key = ? AND timestamp >= ?',
(key, earliest_acceptable.isoformat()), (key, earliest_acceptable),
).fetchall(): ).fetchall():
case [(value,)]: case [(value,)]:
return value return value
@ -20,7 +20,7 @@ class Cache:
self.db.execute( self.db.execute(
'INSERT INTO cache (key, timestamp, value) VALUES (?, ?, ?) ' + 'INSERT INTO cache (key, timestamp, value) VALUES (?, ?, ?) ' +
'ON CONFLICT(key) DO UPDATE SET timestamp = excluded.timestamp, value = excluded.value', 'ON CONFLICT(key) DO UPDATE SET timestamp = excluded.timestamp, value = excluded.value',
(key, now.isoformat(), value), (key, now, value),
) )
return value return value

60
ptv.py
View File

@ -1,24 +1,26 @@
from collections import defaultdict from datetime import datetime, timedelta
from datetime import datetime
from hashlib import sha1 from hashlib import sha1
import hmac import hmac
import httpx import httpx
import json
import os import os
dev_id = os.environ['PTV_USER_ID'] dev_id = os.environ['PTV_USER_ID']
api_key = os.environ['PTV_API_KEY'] api_key = os.environ['PTV_API_KEY']
def sign(request): def sign(request):
request += '&' if ('?' in request) else '?' request += '&' if '?' in request else '?'
request += 'devid=' request += 'devid='
request += dev_id request += dev_id
hashed = hmac.new(api_key.encode(), request.encode(), sha1) hashed = hmac.new(api_key.encode(), request.encode(), sha1)
return f'https://timetableapi.ptv.vic.gov.au{request}&signature={hashed.hexdigest().upper()}' return f'https://timetableapi.ptv.vic.gov.au{request}&signature={hashed.hexdigest().upper()}'
def fetch(path): def fetch(path, cache, ttl=timedelta(minutes=5)):
res = httpx.get(sign(path)) def do():
res.raise_for_status() res = httpx.get(sign(path))
return res.json() res.raise_for_status()
return res.text
return json.loads(cache.get(path, ttl, do))
# Lilydale route 9 (type 0) (dir 1 in, 8 out) (stop 1229) # Lilydale route 9 (type 0) (dir 1 in, 8 out) (stop 1229)
# Belgrave route 2 (type 0) (dir 1 in, 2 out) (stop 1229) # Belgrave route 2 (type 0) (dir 1 in, 2 out) (stop 1229)
@ -27,52 +29,34 @@ def fetch(path):
# 766 route 15800 (type 2) (dir 13 N, 207 S) (stop 17861) # 766 route 15800 (type 2) (dir 13 N, 207 S) (stop 17861)
# 612 route 13024 (type 2) (dir 13 N, 158 S) (stop 17861) # 612 route 13024 (type 2) (dir 13 N, 158 S) (stop 17861)
class Route: def fetch_departures(stop_id, route_type_id, cache):
def __init__(self, name, route_id, route_type_id, directions, stops): # TODO: Expand runs
self.name = name return fetch(f'/v3/departures/route_type/{route_type_id}/stop/{stop_id}', cache)['departures']
self.route_id = route_id
self.route_type_id = route_type_id
self.directions = directions
self.stops = stops
local_routes = [ def fetch_run(run_ref, cache):
Route('Lilydale', 9 , 0, [(1, 'in'), (8, 'out')] , [1229] ), return fetch(f'/v3/runs/{run_ref}', cache, timedelta(hours=8))['runs']
Route('Belgrave', 2 , 0, [(1, 'in'), (2, 'out')] , [1229] ),
Route('109' , 722 , 1, [(2, 'E'), (3, 'W')] , [2415, 2460]),
Route('70' , 940 , 1, [(28, 'E'), (29, 'W')] , [2162, 2161]),
Route('766' , 15800, 2, [(13, 'N'), (188, 'S')] , [17861] ),
Route('612' , 13024, 2, [(13, 'N'), (139, 'S')] , [17861] ),
]
# exp 951624
# non-exp 951826
def fetch_departures(stop_id, route_type_id):
return fetch(f'/v3/departures/route_type/{route_type_id}/stop/{stop_id}')['departures']
def fetch_run(run_ref):
return fetch(f'/v3/runs/{run_ref}')['runs']
def departure_time(dep): def departure_time(dep):
return datetime.fromisoformat(dep['estimated_departure_utc'] or dep['scheduled_departure_utc']) return datetime.fromisoformat(dep['estimated_departure_utc'] or dep['scheduled_departure_utc'])
def get_departure_data(): def get_departure_data(now, cache):
bus = fetch_departures(17861, 2) bus = fetch_departures(17861, 2, cache)
train = fetch_departures(1229, 0) train = fetch_departures(1229, 0, cache)
tram_109w = fetch_departures(2460, 1) tram_109w = fetch_departures(2460, 1, cache)
tram_70w = fetch_departures(2161, 1) tram_70w = fetch_departures(2161, 1, cache)
next_express_dep = None next_express_dep = None
train.sort(key=departure_time) train.sort(key=departure_time)
for dep in train: for dep in train:
if dep['direction_id'] == 1: if dep['direction_id'] == 1 and departure_time(dep) > now:
match fetch_run(dep['run_ref']): match fetch_run(dep['run_ref'], cache):
case [run]: case [run]:
if run['express_stop_count'] > 1: # Ignore East Richmond if run['express_stop_count'] > 1: # Ignore East Richmond
next_express_dep = dep next_express_dep = dep
break break
def earliest(deps): def earliest(deps):
return min((departure_time(dep) for dep in deps), default=None) return min((departure_time(dep) for dep in deps if departure_time(dep) > now), default=None)
return { return {
'612 S': earliest(dep for dep in bus if dep['route_id'] == 13024 and dep['direction_id'] == 139), '612 S': earliest(dep for dep in bus if dep['route_id'] == 13024 and dep['direction_id'] == 139),