Implement transit time display
This commit is contained in:
parent
f8cc9f213c
commit
b837fcac02
34
__main__.py
34
__main__.py
@ -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
|
||||||
|
|||||||
8
cache.py
8
cache.py
@ -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
60
ptv.py
@ -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),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user