From 8833e548ebfe6dec34b0cd481ddb34950fb337d1 Mon Sep 17 00:00:00 2001 From: David Rice Date: Thu, 16 Apr 2026 20:15:19 +0100 Subject: [PATCH] bacon --- display.py | 91 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 48 insertions(+), 43 deletions(-) diff --git a/display.py b/display.py index bfcdcc4..c4e227a 100644 --- a/display.py +++ b/display.py @@ -8,7 +8,7 @@ Renders the JARVIS UI overlay via X11. Kiosk: DISPLAY=:0 python display.py --fullscreen Weather via Open-Meteo (free, no API key). -Location set by --location postcode (default BH8 8JZ). +Cycles through _LOCATIONS every 5 seconds. """ import os @@ -28,12 +28,13 @@ _parser.add_argument('--width', type=int, default=1280, help='Screen width (default 1280)') _parser.add_argument('--height', type=int, default=800, help='Screen height (default 800)') -_parser.add_argument('--location', type=str, default='BH8 8JZ', - help='UK postcode for weather (default BH8 8JZ)') _args, _ = _parser.parse_known_args() import pygame +# ── Locations to cycle through ──────────────────────────────────────────────── +_LOCATIONS = ['Poole', 'Portsmouth', 'Besancon', 'Paris', 'Gorinchem'] + # ── Colour palette ─────────────────────────────────────────────────────────── BLACK = ( 0, 0, 0) WHITE = (255, 255, 255) @@ -54,27 +55,27 @@ _WMO = { 95: 'Thunderstorm', 96: 'Thunderstorm + hail', 99: 'Thunderstorm + hail', } -# ── Weather state (updated by background thread) ────────────────────────────── -_weather: dict = {} # keys: 'week' (list), 'hours' (list), 'location' (str) +# ── Weather state — one dict per location, updated by background thread ─────── +_weather_all: list[dict] = [] _weather_lock = threading.Lock() -def _postcode_to_latlon(postcode: str) -> tuple[float, float, str]: - url = 'https://api.postcodes.io/postcodes/' + urllib.parse.quote(postcode.replace(' ', '')) +def _geocode(city: str) -> tuple[float, float, str]: + url = ('https://geocoding-api.open-meteo.com/v1/search' + f'?name={urllib.parse.quote(city)}&count=1&language=en&format=json') with urllib.request.urlopen(url, timeout=10) as r: data = json.loads(r.read()) - result = data['result'] - town = result.get('admin_district', postcode).split(',')[0].strip() - return result['latitude'], result['longitude'], town + r = data['results'][0] + return r['latitude'], r['longitude'], r['name'] -def _fetch_weather(lat: float, lon: float) -> None: +def _fetch_one(lat: float, lon: float) -> dict: url = ( 'https://api.open-meteo.com/v1/forecast' f'?latitude={lat:.4f}&longitude={lon:.4f}' '&daily=weather_code,temperature_2m_max,temperature_2m_min' '&hourly=temperature_2m,weather_code' - '&timezone=Europe%2FLondon' + '&timezone=auto' '&forecast_days=7' ) with urllib.request.urlopen(url, timeout=10) as r: @@ -88,7 +89,6 @@ def _fetch_weather(lat: float, lon: float) -> None: d = datetime.strptime(date_str, '%Y-%m-%d') week.append({ 'day': 'Today' if i == 0 else d.strftime('%A'), - 'date': d.strftime('%-d %b'), 'high': round(daily['temperature_2m_max'][i]), 'low': round(daily['temperature_2m_min'][i]), 'desc': _WMO.get(daily['weather_code'][i], ''), @@ -108,17 +108,22 @@ def _fetch_weather(lat: float, lon: float) -> None: 'desc': _WMO.get(hourly['weather_code'][i], ''), }) - with _weather_lock: - _weather['week'] = week - _weather['hours'] = hours + return {'week': week, 'hours': hours} -def _weather_worker(lat: float, lon: float) -> None: +def _weather_worker(locations: list[tuple[float, float, str]]) -> None: while True: - try: - _fetch_weather(lat, lon) - except Exception: - pass + fresh = [] + for lat, lon, name in locations: + try: + entry = _fetch_one(lat, lon) + except Exception: + entry = {'week': [], 'hours': []} + entry['town'] = name + fresh.append(entry) + with _weather_lock: + _weather_all.clear() + _weather_all.extend(fresh) _time.sleep(1800) @@ -191,17 +196,16 @@ def draw_clock(surface: pygame.Surface, fonts: dict, x: int, y: int) -> None: # ── Weather panel ───────────────────────────────────────────────────────────── _WEATHER_COLS = 5 -_WEATHER_COL_W = 84 # px per column — 5 cols = 420px total +_WEATHER_COL_W = 84 def draw_weather(surface: pygame.Surface, fonts: dict, x: int, y: int) -> None: with _weather_lock: - week = list(_weather.get('week', [])) - hours = list(_weather.get('hours', [])) + all_data = list(_weather_all) - th = fonts['tiny'].get_height() - row = th + 2 - town = _weather.get('town', '') + th = fonts['tiny'].get_height() + row = th + 2 + town_h = fonts['medium'].get_height() def _trunc(s, n=13): return s if len(s) <= n else s[:n-1] + '…' @@ -213,15 +217,19 @@ def draw_weather(surface: pygame.Surface, fonts: dict, x: int, y: int) -> None: surface.blit(fonts['tiny'].render(row2_fn(item), True, WHITE), (cx, start_y + row)) surface.blit(fonts['tiny'].render(row3_fn(item), True, DIM_GRAY), (cx, start_y + 2*row)) - # Town name header - town_h = fonts['medium'].get_height() - if town: - surface.blit(fonts['medium'].render(town, True, GRAY), (x, y)) - - if not week: - surface.blit(fonts['tiny'].render('Loading…', True, DIM_GRAY), (x, y + town_h + 4)) + if not all_data: + surface.blit(fonts['medium'].render('Weather loading…', True, DIM_GRAY), (x, y)) return + idx = int(_time.time() / 5) % len(all_data) + data = all_data[idx] + week = data.get('week', []) + hours = data.get('hours', []) + town = data.get('town', '') + + # Town name + surface.blit(fonts['medium'].render(town, True, GRAY), (x, y)) + # 5-day section _col(week, y + town_h + 4, @@ -231,14 +239,13 @@ def draw_weather(surface: pygame.Surface, fonts: dict, x: int, y: int) -> None: # 5-hour section if hours: - _col(hours[:_WEATHER_COLS], + _col(hours, y + town_h + 4 + 3*row + 8, lambda h: h['label'], lambda h: f"{h['temp']}°", lambda h: _trunc(h['desc'])) - # ── Font helper ─────────────────────────────────────────────────────────────── def _load_font(size: int, bold: bool = False) -> pygame.font.Font: @@ -282,13 +289,12 @@ def main() -> None: clock_block_h = fonts['small'].get_height() + 4 + fonts['large'].get_height() clock_y = face_cy - clock_block_h // 2 weather_x = W - _WEATHER_COLS * _WEATHER_COL_W - 15 + weather_y = clock_y - fonts['medium'].get_height() - 4 - # Resolve postcode and start weather thread + # Geocode all locations and start background weather thread try: - lat, lon, town = _postcode_to_latlon(_args.location) - with _weather_lock: - _weather['town'] = town - threading.Thread(target=_weather_worker, args=(lat, lon), daemon=True).start() + resolved = [_geocode(city) for city in _LOCATIONS] + threading.Thread(target=_weather_worker, args=(resolved,), daemon=True).start() except Exception: pass @@ -320,8 +326,7 @@ def main() -> None: draw_wireframe_face(screen, face_cx, face_cy, face_rx, face_ry) draw_clock(screen, fonts, 20, clock_y) - draw_weather(screen, fonts, weather_x, - clock_y - fonts['medium'].get_height() - 4) + draw_weather(screen, fonts, weather_x, weather_y) pygame.display.flip() pg_clock.tick(10)