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