Compare commits

...

98 Commits

Author SHA1 Message Date
alex 7c1350ac01 attempt to improve colors 2026-05-06 18:24:18 -04:00
alex 7e265e7a2a perf 2026-05-06 03:26:08 -04:00
alex 0db43e3135 testing 2026-05-06 03:25:08 -04:00
alex 8d7bc08499 check for league in preferred games 2026-05-06 03:24:34 -04:00
alex bb3aa2e704 update readme 2026-05-06 03:20:43 -04:00
alex 886e18d763 update readme 2026-05-05 18:24:30 -04:00
alex e36994610f testing 2026-05-05 18:22:31 -04:00
alex 5cf53fbb19 testing 2026-05-05 18:20:43 -04:00
alex c8f28930e5 improve paging 2026-05-05 18:17:45 -04:00
alex 7fcd4df164 ooooo 2026-05-05 18:12:41 -04:00
alex 6560509eed wheeew 2026-05-05 18:10:05 -04:00
alex 51d4fb2656 whoooops lol 2026-05-05 18:09:33 -04:00
alex 0345fa1fee fix paging on all games 2026-05-05 18:07:53 -04:00
alex fb310420e7 debug pages 2026-05-05 18:02:50 -04:00
alex 82f2970630 ahhh 2026-05-05 18:00:30 -04:00
alex fa7cad0b4e perchance 2026-05-05 17:57:48 -04:00
alex a9fc06b5c8 fix fucky logic 2026-05-05 17:55:33 -04:00
alex 245ef271c0 don't skip games 2026-05-05 17:53:17 -04:00
alex dd644bc38c don't readd same games 2026-05-05 17:51:57 -04:00
alex 93c610902b more logs 2026-05-05 17:49:50 -04:00
alex 1be8c704f8 test 2026-05-05 17:49:09 -04:00
alex ab179df27c cycle attempt 3.. 2026-05-05 17:47:34 -04:00
alex 2e04da5aa6 wrong var 2026-05-05 17:40:43 -04:00
alex 8233bf4d1b 2nd attempt at cycling 2026-05-05 17:39:47 -04:00
alex 43755aba7b reset currentpage 2026-05-05 17:28:34 -04:00
alex 3817286966 try cycling between both 2026-05-05 17:21:25 -04:00
alex ca5c8d6c1c fix paths 2026-05-05 17:10:25 -04:00
alex ab08c80d80 remove finished games from preferred 2026-05-05 17:02:39 -04:00
alex 0661485c5e add debug logs 2026-05-05 17:00:03 -04:00
alex a6957139e7 clear canvas before drawing 2026-05-05 16:56:37 -04:00
alex 97ee9f5ccd make sleep longer 2026-05-05 16:53:58 -04:00
alex c18382f370 return first item? 2026-05-05 16:53:08 -04:00
alex 5ed0bdf643 print single preferred 2026-05-05 16:52:32 -04:00
alex 26b842668f wrong keyword 2026-05-05 16:51:43 -04:00
alex 61f397076c pass correct thing 2026-05-05 16:51:27 -04:00
alex 95968cdcb6 preferredgame 2026-05-05 16:50:07 -04:00
alex a711450445 preferredgame 2026-05-05 16:49:04 -04:00
alex a5a15de69b more cleanup 2026-05-05 16:46:37 -04:00
alex 3359e89027 improve code 2026-05-05 16:39:13 -04:00
alex 043f44f8fa add support for single games 2026-05-05 16:26:04 -04:00
alex 394ceec192 test 2026-05-03 18:27:37 -04:00
alex 8097bb9a10 test 2026-05-03 18:27:09 -04:00
alex 2ee6517162 test 2026-05-03 18:26:46 -04:00
alex a4d79d4536 discard when cycling 2026-05-03 18:24:27 -04:00
alex 48c1db9ddb whoops 2026-05-03 18:22:28 -04:00
alex a64f089054 fix current page 2026-05-03 18:22:08 -04:00
alex dcc42e1aa1 pass required args 2026-05-03 18:19:41 -04:00
alex 6f12614c36 test 2026-05-03 18:18:09 -04:00
alex 525c70cf91 cycle through all preferred games 2026-05-03 18:17:03 -04:00
alex 6bb1f74cf3 slow down 2026-05-03 14:56:57 -04:00
alex 07a0bbab6e fix preferred game continuing 2026-05-03 14:55:19 -04:00
alex d799459d50 test 2 2026-05-03 14:54:40 -04:00
alex d7ee473c15 clean up 2026-05-03 14:54:05 -04:00
alex d7b258bb51 move text 2026-05-03 14:47:54 -04:00
alex 0bfac21b92 move text 2026-05-03 14:47:38 -04:00
alex 386fdfc04a move text 2026-05-03 14:46:52 -04:00
alex 7cde8237a0 move text 2026-05-03 14:46:37 -04:00
alex 9c48a95ed3 move text 2026-05-03 14:46:13 -04:00
alex 0f8e7dce1f move text 2026-05-03 14:45:52 -04:00
alex 69f15bdfc1 move text 2026-05-03 14:45:28 -04:00
alex e33495e525 add status 2026-05-03 14:44:40 -04:00
alex a4093b019d add text 2026-05-03 14:41:44 -04:00
alex fcee870ec3 would help if i actually drew it 2026-05-03 14:35:31 -04:00
alex 57aa924e7f test specific game monitoring 2026-05-03 14:34:18 -04:00
alex efbb6f5737 diff log 2026-05-03 14:26:24 -04:00
alex 6ac11cb4a8 format with ruff 2026-05-03 14:25:53 -04:00
alex 11a6abdf10 print game statuses 2026-05-03 05:10:07 -04:00
alex e62630b7e3 fix clear func 2026-05-03 05:06:29 -04:00
alex 886128c9c9 another value 2026-05-03 05:05:28 -04:00
alex db2540af6e fix value 2026-05-03 05:04:11 -04:00
alex 8afd56c9c2 fix args 2026-05-03 05:02:19 -04:00
alex 2dd786bded fix wrong func 2026-05-03 05:01:38 -04:00
alex 972cd94305 test 2026-05-03 05:01:06 -04:00
alex a7ad381e3a okay fine 2026-05-03 04:58:10 -04:00
alex 198bf89356 wonder if thats better 2026-05-03 04:57:43 -04:00
alex e23182f040 maybe this was right? 2026-05-03 04:56:59 -04:00
alex f2fe63c944 okay fine 2026-05-03 04:56:24 -04:00
alex 83f67e2156 expand tuple 2026-05-03 04:55:30 -04:00
alex 8dd43a85a6 put in graphics.Color 2026-05-03 04:54:31 -04:00
alex f0f8fdc01a bad usage 2026-05-03 04:52:34 -04:00
alex ba47ae0952 house cleaning 2026-05-03 03:27:57 -04:00
alex 920e11acdf hold for longer 2026-05-02 23:59:15 -04:00
alex 8aaeb8a393 don't clear 2026-05-02 23:58:20 -04:00
alex e5f4c07c16 for testing 2026-05-02 23:57:03 -04:00
alex 2faa7b7982 readd audio 2026-05-02 23:50:15 -04:00
alex 43a22359df i silly mannn 2026-05-02 23:40:27 -04:00
alex 2f9fe7cf94 fix? 2026-05-02 23:37:24 -04:00
alex 12a3909f24 fix main script 2026-05-02 23:36:13 -04:00
alex 1599df415b use http api 2026-05-02 23:34:39 -04:00
alex 28db60ad86 best work 2026-05-02 23:16:40 -04:00
alex 9d85a6a76b readd delay 2026-05-02 23:15:45 -04:00
alex 9f81f497e0 remove reset 2026-05-02 23:15:01 -04:00
alex 6690acc346 send scene 2026-05-02 23:13:52 -04:00
alex a8d1ea2b8d fawk 2026-05-02 23:13:19 -04:00
alex dd9e4ba193 fix main 2026-05-02 23:12:53 -04:00
alex 00fbcc1ce6 update govee class 2026-05-02 23:12:12 -04:00
alex f84b52543a await 2026-05-02 23:10:45 -04:00
alex 09df7c6ac3 try to power on then set scene 2026-05-02 23:09:55 -04:00
12 changed files with 210035 additions and 247 deletions
+3
View File
@@ -0,0 +1,3 @@
GOVEE_API_KEY=
GOVEE_DEVICE=
GOVEE_SKU=
+2
View File
@@ -1 +1,3 @@
__pycache__ __pycache__
.env
logos
+25905
View File
File diff suppressed because it is too large Load Diff
+64553
View File
File diff suppressed because it is too large Load Diff
+119182
View File
File diff suppressed because it is too large Load Diff
View File
+8 -3
View File
@@ -7,12 +7,13 @@ LEAGUES = [
("hockey", "nhl"), ("hockey", "nhl"),
("football", "nfl"), ("football", "nfl"),
("basketball", "nba"), ("basketball", "nba"),
("baseball", "mlb"),
] ]
LOGO_DIR = "/home/alex/logos"
LOGO_SIZE = (16, 16) LOGO_SIZE = (16, 16)
os.makedirs(LOGO_DIR, exist_ok=True) os.makedirs("./assets/logos", exist_ok=True)
def download_logos(sport, league): def download_logos(sport, league):
url = f"https://site.api.espn.com/apis/site/v2/sports/{sport}/{league}/teams" url = f"https://site.api.espn.com/apis/site/v2/sports/{sport}/{league}/teams"
@@ -28,6 +29,9 @@ def download_logos(sport, league):
if not logo_url: if not logo_url:
print(f"No logo for {abbr}, skipping") print(f"No logo for {abbr}, skipping")
continue continue
if os.path.exists(os.path.join("./assets/logos", f"{league}_{abbr}.png")):
print(f"Logo exists for {abbr}, skipping")
continue
try: try:
img_resp = requests.get(logo_url, timeout=10) img_resp = requests.get(logo_url, timeout=10)
@@ -40,12 +44,13 @@ def download_logos(sport, league):
background = Image.new("RGB", LOGO_SIZE, (0, 0, 0)) background = Image.new("RGB", LOGO_SIZE, (0, 0, 0))
background.paste(img, mask=img.split()[3]) background.paste(img, mask=img.split()[3])
out_path = os.path.join(LOGO_DIR, f"{league}_{abbr}.png") out_path = os.path.join("./assets/logos", f"{league}_{abbr}.png")
background.save(out_path) background.save(out_path)
print(f"Saved {out_path}") print(f"Saved {out_path}")
except Exception as e: except Exception as e:
print(f"Error downloading {abbr}: {e}") print(f"Error downloading {abbr}: {e}")
for sport, league in LEAGUES: for sport, league in LEAGUES:
print(f"\nDownloading {league.upper()} logos...") print(f"\nDownloading {league.upper()} logos...")
download_logos(sport, league) download_logos(sport, league)
+66 -74
View File
@@ -1,89 +1,81 @@
import asyncio import requests
import random
import string
import json import json
import time
from typing import Optional
GOVEE_LAN_PORT = 4003 GOVEE_API_BASE_URL = "https://openapi.api.govee.com/router/api/v1/"
INTER_SEGMENT_DELAY = 0.1
class GoveeProtocol(asyncio.DatagramProtocol):
def __init__(self):
self.transport = None
self._pending: asyncio.Future | None = None
def connection_made(self, transport):
self.transport = transport
def datagram_received(self, data, addr):
if self._pending and not self._pending.done():
self._pending.set_result(json.loads(data.decode()))
def error_received(self, exc):
if self._pending and not self._pending.done():
self._pending.set_exception(exc)
class GoveeApi: class GoveeApi:
def __init__(self, device_ip: str, retries: int = 3, retry_delay: float = 0.05): key = ""
self.device_ip = device_ip headers = {}
self.retries = retries
self.retry_delay = retry_delay
self._protocol: Optional[GoveeProtocol] = None
async def connect(self): def __init__(self, key):
loop = asyncio.get_running_loop() self.key = key
_, self._protocol = await loop.create_datagram_endpoint( self.headers = {"Govee-API-Key": key}
GoveeProtocol,
remote_addr=(self.device_ip, GOVEE_LAN_PORT)
)
async def close(self): def __get_random_string(self):
if self._protocol and self._protocol.transport: characters = string.ascii_letters + string.digits
self._protocol.transport.close() result_str = "".join(random.choices(characters, k=4))
return result_str
async def __aenter__(self): def get_diy_scenes(self, sku, device):
await self.connect()
return self
async def __aexit__(self, *args):
await self.close()
async def send_scene(self, scene_code: str):
segments = scene_code.split(",")
for i, segment in enumerate(segments):
payload = {"msg": {"cmd": "ptReal", "data": {"command": [segment]}}}
await self._send(payload)
if i < len(segments) - 1:
await asyncio.sleep(INTER_SEGMENT_DELAY)
async def set_color(self, r: int, g: int, b: int, kelvin: int = 0):
payload = { payload = {
"msg": { "requestId": self.__get_random_string(),
"cmd": "colorwc", "payload": {"sku": sku, "device": device},
"data": {
"color": {"r": r, "g": g, "b": b},
"colorTemInKelvin": kelvin
} }
res = requests.post(
GOVEE_API_BASE_URL + "device/diy-scenes", json=payload, headers=self.headers
)
res.raise_for_status()
print("[GOVEE] DIY scene fetch: " + json.dumps(res.json(), indent=4))
return res.ok
def set_diy_scene(self, sku, device):
payload = {
"requestId": self.__get_random_string(),
"payload": {
"sku": sku,
"device": device,
"capability": {
"type": "device.capabilities.dynamic_scene",
"instance": "diyScene",
"value": 22757907,
},
},
} }
res = requests.post(
GOVEE_API_BASE_URL + "device/control", headers=self.headers, json=payload
)
res.raise_for_status()
print("[GOVEE] Set DIY scene: " + json.dumps(res.json(), indent=4))
return res.ok
def set_to_original_color(self, sku, device):
payload = {
"requestId": self.__get_random_string(),
"payload": {
"sku": sku,
"device": device,
"capability": {
"type": "devices.capabilities.color_setting",
"instance": "colorRgb",
"value": 16711680,
},
},
} }
await self._send(payload)
async def _send(self, payload: dict) -> Optional[dict]: res = requests.post(
if not self._protocol: GOVEE_API_BASE_URL + "device/control", json=payload, headers=self.headers
raise RuntimeError("Not connected — use async with or call connect() first") )
res.raise_for_status()
data = json.dumps(payload).encode() print("[GOVEE] Set to original: " + json.dumps(res.json(), indent=4))
loop = asyncio.get_running_loop()
for attempt in range(self.retries): return res.ok
future: asyncio.Future = loop.create_future()
self._protocol._pending = future
self._protocol.transport.sendto(data)
try:
return await asyncio.wait_for(future, timeout=1.0)
except asyncio.TimeoutError:
if attempt < self.retries - 1:
await asyncio.sleep(self.retry_delay)
print(f"Warning: no response after {self.retries} attempts")
return None
+50
View File
@@ -0,0 +1,50 @@
# scoreboard
Scoreboard script for showing active sports I like.
## Equipment
I have a small setup to make this work, all specs found below
- [Raspberry Pi 4, 2GB](https://www.amazon.com/dp/B07TYK4RL8?ref=ppx_yo2ov_dt_b_fed_asin_title)
- [4x waveshare P2.5 64x32 LED panels](https://www.amazon.com/dp/B0BRBGHFKQ?ref=ppx_yo2ov_dt_b_fed_asin_title)
- [MEAN WELL LRS-100-5 5v PSU](https://www.amazon.com/dp/B07DKCMJN9?ref=ppx_yo2ov_dt_b_fed_asin_title)
- [Adafruit Triple LED Matrix HUB75 Bonnet](https://www.adafruit.com/product/6358)
## Setup
- Install Python, Python build tools & build rpi-rgb-led-matrix [from source](https://github.com/hzeller/rpi-rgb-led-matrix) or install it via pip
- Clone this repository
- Install requirements using `pip install -r requirements.txt`
- Download logos using the `download_logos.py` Python script
- Run `scoreboard.py` Python script
This runs as a systemd service on my Pi, with the following configuration
```
[Unit]
Description=Scoreboard Script
After=multi-user.target
[Service]
ExecStart=/usr/bin/python3 scoreboard.py
KillSignal=SIGINT
WorkingDirectory=/home/pi/Documents/scoreboard
Restart=always
User=pi
[Install]
WantedBy=multi-user.target
```
Make usre to update paths accordingly.
## Adding more leagues/teams
Simply add the league & sport to the `get_all_scores()` function in scoreboard.py. These should be relatively self-explanitory knowing the league name/sport name.
Make sure you get logos as well, doing the same in download_logos.py and running the script again.
## Govee Lighting
If you have Govee lighting you want to setup for celebrations, create a `.env` file and complete the arguments in the template file.
+4
View File
@@ -0,0 +1,4 @@
pygame
requests
python-dotenv
pillow
+227 -131
View File
@@ -1,11 +1,24 @@
import time
import requests import requests
import os import os
import govee import govee
import asyncio import math
from time import sleep import pygame
from enum import Enum
from time import sleep, time
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from rgbmatrix import RGBMatrix, RGBMatrixOptions, graphics from rgbmatrix import RGBMatrix, RGBMatrixOptions, graphics
from dotenv import load_dotenv
# --- Current file path for file resolution
SCRIPT_DIR = Path(__file__).parent.resolve()
# --- Load environment vars ---
load_dotenv()
# --- Default vars ---
ASSET_DIR = os.path.join(SCRIPT_DIR, "assets")
LOGO_DIR = os.path.join(ASSET_DIR, "logos")
# --- Matrix config --- # --- Matrix config ---
options = RGBMatrixOptions() options = RGBMatrixOptions()
@@ -13,7 +26,7 @@ options.rows = 32
options.cols = 64 options.cols = 64
options.chain_length = 4 options.chain_length = 4
options.parallel = 1 options.parallel = 1
options.hardware_mapping = 'regular' options.hardware_mapping = "regular"
options.gpio_slowdown = 5 options.gpio_slowdown = 5
options.disable_hardware_pulsing = True options.disable_hardware_pulsing = True
options.brightness = 80 options.brightness = 80
@@ -21,46 +34,43 @@ options.brightness = 80
matrix = RGBMatrix(options=options) matrix = RGBMatrix(options=options)
canvas = matrix.CreateFrameCanvas() canvas = matrix.CreateFrameCanvas()
# --- Font initialization ---
font = graphics.Font() font = graphics.Font()
font.LoadFont("/usr/local/share/7x13.bdf")
font_small = graphics.Font() font_small = graphics.Font()
font_small.LoadFont("/usr/local/share/5x7.bdf")
# try to load a big font for GOAL!, fall back to regular if not available
font_big = graphics.Font() font_big = graphics.Font()
try: font.LoadFont(os.path.join(ASSET_DIR, "fonts/7x13.bdf"))
font_big.LoadFont("/usr/local/share/9x18.bdf") font_small.LoadFont(os.path.join(ASSET_DIR, "fonts/5x7.bdf"))
except: font_big.LoadFont(os.path.join(ASSET_DIR, "fonts/9x18.bdf"))
font_big = font
white = graphics.Color(255, 255, 255)
yellow = graphics.Color(255, 200, 0)
red = graphics.Color(255, 50, 50)
grey = graphics.Color(180, 180, 180)
sabres_blue = graphics.Color(0, 135, 48)
sabres_gold = graphics.Color(252, 20, 210)
LOGO_DIR = "/usr/local/share/logos"
SABRES_ABBR = "BUF"
GOVEE_DEVICE = "3D:22:D7:94:40:46:2F:72"
GOVEE_SKU = "H6168"
GOVEE_AWS = "owABAgT/AGQMACr//+YAADb//8k=,o//QAAIDAwAAAAAAAAAAAAAAAI4=,MwUKdwAAAAAAAAAAAAAAAAAAAEs="
GOVEE_IP = "172.16.0.15"
# --- Logo cache --- # --- Logo cache ---
logo_cache = {} logo_cache = {}
# --- Govee API --- # --- Pre-built colors ---
govee_api = govee.GoveeApi(device_ip=GOVEE_IP) class Colors(Enum):
WHITE = (255, 255, 255)
YELLOW = (255, 200, 0)
RED = (255, 50, 50)
SABRES_BLUE = (0, 135, 48)
SABRES_GOLD = (252, 20, 210)
# --- Govee API ---
if os.environ['GOVEE_API_KEY']:
govee_api = govee.GoveeApi(key=os.environ["GOVEE_API_KEY"])
# --- PyGame Audio ---
pygame.mixer.init()
# --- Goal celebrations ---
def render_goal_frame(text, text_scale, bg_color, text_color): def render_goal_frame(text, text_scale, bg_color, text_color):
big_h = max(8, int(32 * text_scale)) big_h = max(8, int(32 * text_scale))
big_img = Image.new("RGB", (1024, 128), bg_color) big_img = Image.new("RGB", (1024, 128), bg_color)
big_draw = ImageDraw.Draw(big_img) big_draw = ImageDraw.Draw(big_img)
# rpi specific, fall back to default font if not existing
try: try:
pil_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", big_h) pil_font = ImageFont.truetype(
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", big_h
)
except: except:
pil_font = ImageFont.load_default() pil_font = ImageFont.load_default()
@@ -75,7 +85,6 @@ def render_goal_frame(text, text_scale, bg_color, text_color):
scaled = big_img.resize((256, 32), Image.LANCZOS) scaled = big_img.resize((256, 32), Image.LANCZOS)
# paste sabres logo on left and right # paste sabres logo on left and right
# Replace the logo paste section with this
logo_path = os.path.join(LOGO_DIR, "nhl_BUF.png") logo_path = os.path.join(LOGO_DIR, "nhl_BUF.png")
if os.path.exists(logo_path): if os.path.exists(logo_path):
try: try:
@@ -108,65 +117,72 @@ def render_goal_frame(text, text_scale, bg_color, text_color):
return scaled return scaled
def draw_pil_image(canvas, img): def play_goal_celebration(text, color1, color2):
for x in range(img.width):
for y in range(img.height):
r, g, b = img.getpixel((x, y))
canvas.SetPixel(x, y, b, g, r) # swap r and g for GRB panels
def play_goal_celebration():
global canvas global canvas
BLUE = (0, 135, 48) # was (0, 48, 135)
GOLD = (252, 20, 210) # was (252, 210, 20)
WHITE = (255, 255, 255) # unchanged
TEXT = "SABRES GOAL!"
# Phase 1: zoom in from tiny to full, alternating bg color # Phase 1: zoom in from tiny to full, alternating bg color
zoom_steps = [0.1, 0.2, 0.35, 0.5, 0.65, 0.8, 0.95, 1.1, 1.0] zoom_steps = [0.1, 0.2, 0.35, 0.5, 0.65, 0.8, 0.95, 1.1, 1.0]
for _ in range(5):
for i, scale in enumerate(zoom_steps): for i, scale in enumerate(zoom_steps):
bg = BLUE if i % 2 == 0 else GOLD bg = color1 if i % 2 == 0 else color2
fg = GOLD if i % 2 == 0 else BLUE fg = color2 if i % 2 == 0 else color1
frame = render_goal_frame(TEXT, scale, bg, fg) frame = render_goal_frame(text, scale, bg, fg)
canvas.Clear() canvas.Clear()
draw_pil_image(canvas, frame) draw_pil_image(canvas, frame)
canvas = matrix.SwapOnVSync(canvas) canvas = matrix.SwapOnVSync(canvas)
time.sleep(0.05) sleep(0.05)
# Phase 2: rapid flashing at full size # Phase 2: rapid flashing at full size
for i in range(10): for i in range(10):
bg = BLUE if i % 2 == 0 else GOLD bg = color1 if i % 2 == 0 else color2
fg = GOLD if i % 2 == 0 else BLUE fg = color2 if i % 2 == 0 else color1
frame = render_goal_frame(TEXT, 1.0, bg, fg) frame = render_goal_frame(text, 1.0, bg, fg)
canvas.Clear() canvas.Clear()
draw_pil_image(canvas, frame) draw_pil_image(canvas, frame)
canvas = matrix.SwapOnVSync(canvas) canvas = matrix.SwapOnVSync(canvas)
time.sleep(0.12) sleep(0.12)
# Phase 3: zoom back out and fade to white flash # Phase 3: zoom back out and fade to white flash
zoom_out = [1.0, 1.1, 1.2, 1.3, 1.4] zoom_out = [1.0, 1.1, 1.2, 1.3, 1.4]
for i, scale in enumerate(zoom_out): for i, scale in enumerate(zoom_out):
bg = GOLD if i % 2 == 0 else BLUE bg = color2 if i % 2 == 0 else color1
fg = BLUE if i % 2 == 0 else GOLD fg = color1 if i % 2 == 0 else color2
frame = render_goal_frame(TEXT, scale, bg, fg) frame = render_goal_frame(text, scale, bg, fg)
canvas.Clear() canvas.Clear()
draw_pil_image(canvas, frame) draw_pil_image(canvas, frame)
canvas = matrix.SwapOnVSync(canvas) canvas = matrix.SwapOnVSync(canvas)
time.sleep(0.08) sleep(0.08)
# Phase 4: white flash to end # Phase 4: white flash to end
for _ in range(3): for _ in range(3):
canvas.Clear() canvas.Clear()
frame = render_goal_frame(TEXT, 1.0, WHITE, BLUE) frame = render_goal_frame(
text, 1.0, Colors.SABRES_GOLD.value, Colors.SABRES_BLUE.value
)
draw_pil_image(canvas, frame) draw_pil_image(canvas, frame)
canvas = matrix.SwapOnVSync(canvas) canvas = matrix.SwapOnVSync(canvas)
time.sleep(0.1) sleep(0.1)
canvas.Clear()
# clear board
canvas = canvas.Clear()
canvas = matrix.SwapOnVSync(canvas) canvas = matrix.SwapOnVSync(canvas)
time.sleep(0.08)
# stop music if playing
pygame.mixer.music.stop()
# Hold for a moment then return to scoreboard # Hold for a moment then return to scoreboard
time.sleep(0.5) sleep(0.5)
def play_audio(filename):
pygame.mixer.music.load(os.path.join(ASSET_DIR, filename))
pygame.mixer.music.play()
# --- Utilities ---
def draw_pil_image(canvas, img):
for x in range(img.width):
for y in range(img.height):
r, g, b = img.getpixel((x, y))
canvas.SetPixel(x, y, b, g, r) # bgr panels
def load_logo(league, abbr): def load_logo(league, abbr):
key = f"{league}_{abbr}" key = f"{league}_{abbr}"
@@ -194,7 +210,7 @@ def draw_logo(canvas, img, x, y):
for px in range(img.width): for px in range(img.width):
for py in range(img.height): for py in range(img.height):
r, g, b = img.getpixel((px, py)) r, g, b = img.getpixel((px, py))
canvas.SetPixel(x + px, y + py, r, b, g) # RBG order canvas.SetPixel(x + px, y + py, r, b, g) # bgr panels
# --- Fetch scores --- # --- Fetch scores ---
def get_scores(sport, league): def get_scores(sport, league):
@@ -209,7 +225,8 @@ def get_scores(sport, league):
home = next(t for t in teams if t["homeAway"] == "home") home = next(t for t in teams if t["homeAway"] == "home")
away = next(t for t in teams if t["homeAway"] == "away") away = next(t for t in teams if t["homeAway"] == "away")
status = event["status"]["type"]["shortDetail"] status = event["status"]["type"]["shortDetail"]
games.append({ games.append(
{
"league": league, "league": league,
"away": away["team"]["abbreviation"].upper(), "away": away["team"]["abbreviation"].upper(),
"away_score": away["score"], "away_score": away["score"],
@@ -217,47 +234,23 @@ def get_scores(sport, league):
"home_score": home["score"], "home_score": home["score"],
"status": status, "status": status,
"id": event["id"], "id": event["id"],
}) }
)
return games return games
except Exception as e: except Exception as e:
print(f"Fetch error ({league}): {e}") print(f"Fetch error ({league}): {e}")
return [] return []
def get_all_scores(): def get_all_scores():
print('fetching game scores from espn')
games = [] games = []
games += get_scores("hockey", "nhl") games += get_scores("hockey", "nhl")
games += get_scores("football", "nfl") games += get_scores("football", "nfl")
games += get_scores("basketball", "nba") games += get_scores("basketball", "nba")
games += get_scores("baseball", "mlb")
return games return games
def sabres_scored(games, prev_scores): # --- Game drawing ---
for game in games:
if game["away"] != SABRES_ABBR and game["home"] != SABRES_ABBR:
continue
gid = game["id"]
try:
away = int(game["away_score"])
home = int(game["home_score"])
except ValueError:
continue
if gid not in prev_scores:
continue
prev_away, prev_home = prev_scores[gid]
if game["away"] == SABRES_ABBR and away > prev_away:
return True
if game["home"] == SABRES_ABBR and home > prev_home:
return True
return False
# --- Goal celebration ---
def fill_background(canvas, color):
for x in range(256):
for y in range(32):
canvas.SetPixel(x, y, *color)
# --- Draw all games across all panels ---
def draw_all_games(canvas, games, start_index): def draw_all_games(canvas, games, start_index):
for i in range(4): for i in range(4):
game_index = (start_index + i) % len(games) game_index = (start_index + i) % len(games)
@@ -271,68 +264,171 @@ def draw_all_games(canvas, games, start_index):
draw_logo(canvas, away_logo, offset + 0, 0) draw_logo(canvas, away_logo, offset + 0, 0)
draw_logo(canvas, home_logo, offset + 0, 16) draw_logo(canvas, home_logo, offset + 0, 16)
graphics.DrawText(canvas, font_small, offset + 18, 11, white, game["away"]) graphics.DrawText(
graphics.DrawText(canvas, font_small, offset + 18, 27, white, game["home"]) canvas,
font_small,
offset + 18,
11,
graphics.Color(*Colors.RED.value),
game["away"],
)
graphics.DrawText(
canvas,
font_small,
offset + 18,
27,
graphics.Color(*Colors.WHITE.value),
game["home"],
)
graphics.DrawText(canvas, font, offset + 40, 13, yellow, str(game["away_score"])) graphics.DrawText(
graphics.DrawText(canvas, font, offset + 40, 29, yellow, str(game["home_score"])) canvas,
font,
offset + 40,
13,
graphics.Color(*Colors.WHITE.value),
str(game["away_score"]),
)
graphics.DrawText(
canvas,
font,
offset + 40,
29,
graphics.Color(*Colors.WHITE.value),
str(game["home_score"]),
)
if i < 3: if i < 3:
for row in range(32): for row in range(32):
canvas.SetPixel(offset + 63, row, 40, 40, 40) canvas.SetPixel(offset + 63, row, 40, 40, 40)
def draw_single_game(canvas, game):
league = game["league"]
home_logo = load_logo(league, game["home"])
away_logo = load_logo(league, game["away"])
draw_logo(canvas, away_logo, 0, 0)
draw_logo(canvas, home_logo, 0, 16)
graphics.DrawText(
canvas,
font_small,
18,
11,
graphics.Color(*Colors.WHITE.value),
game["away"],
)
graphics.DrawText(
canvas,
font_small,
18,
27,
graphics.Color(*Colors.WHITE.value),
game["home"],
)
graphics.DrawText(
canvas,
font,
40,
13,
graphics.Color(*Colors.WHITE.value),
str(game["away_score"]),
)
graphics.DrawText(
canvas,
font,
40,
29,
graphics.Color(*Colors.WHITE.value),
str(game["home_score"]),
)
graphics.DrawText(
canvas,
font,
55,
22,
graphics.Color(*Colors.WHITE.value),
str(game["status"])
)
# --- Main loop --- # --- Main loop ---
async def run(): def run():
global canvas global canvas
games = [] games = []
prev_scores = {}
last_fetch = 0 last_fetch = 0
current_page = 0 current_page = 0
page_display_time = 8 page_display_time = 8
last_switch = time.time() last_switch = time()
show_preferred = True
# preferred games
current_preferred_game = 0
preferred_games = []
preferred_teams = [
("BUF", "nfl"),
("BUF", "nhl"),
("TOR", "mlb"),
("LAL", "nba"),
]
while True: while True:
async with govee_api: now = time()
govee_api.send_scene(GOVEE_AWS)
play_goal_celebration()
await govee_api.set_color(255,0,0)
# while True: if now - last_fetch > 30 or len(games) <= 0:
# now = time.time() games = get_all_scores()
last_fetch = now
# if now - last_fetch > 30: if games:
# new_games = get_all_scores() canvas.Clear()
# clear bad preferred games out
if len(preferred_games) > 0:
for preferred_game in preferred_games:
shown_game = [g for g in games if preferred_game == g['id']]
if len(shown_game) <= 0 or "Final" in shown_game[0]['status']:
preferred_games.remove(preferred_game)
# # check for sabres goal before updating prev_scores # get new preferred games
# if prev_scores and sabres_scored(new_games, prev_scores): for game in games:
# play_goal_celebration() if (game['away'], game['league']) in preferred_teams or (game['home'], game['league']) in preferred_teams:
if 'Final' not in game['status'] and game['id'] not in preferred_games:
preferred_games.append(game['id'])
# # update prev_scores print(preferred_games)
# for game in new_games: if now - last_switch > page_display_time:
# gid = game["id"] last_switch = now
# try:
# prev_scores[gid] = (int(game["away_score"]), int(game["home_score"]))
# except ValueError:
# pass
# games = new_games if show_preferred and len(preferred_games) > 0:
# last_fetch = now single_preferred_game = next(
# if not games: (g for g in games if g['id'] == preferred_games[current_preferred_game]), None
# current_page = 0 )
if single_preferred_game:
print(f'Showing preferred game {single_preferred_game["home"]} vs {single_preferred_game["away"]}')
draw_single_game(canvas, single_preferred_game)
# if games and now - last_switch > page_display_time: current_preferred_game += 1
# current_page = (current_page + 4) % max(len(games), 1) if current_preferred_game >= len(preferred_games):
# last_switch = now show_preferred = False
current_preferred_game = 0
current_page = 0
else:
print(f'Showing all games page {current_page} / {len(games)}')
draw_all_games(canvas, games, current_page)
current_page += 4
if current_page >= len(games):
current_page = 0
if len(preferred_games) > 0:
show_preferred = True
else:
canvas.Clear()
# canvas.Clear() print('No games available')
# if games: # no games available, just draw placeholder
# draw_all_games(canvas, games, current_page) graphics.DrawText(canvas, font, 10, 22, graphics.Color(*Colors.RED.value), "No games today")
# else:
# graphics.DrawText(canvas, font, 10, 22, red, "No games today")
# canvas = matrix.SwapOnVSync(canvas) canvas = matrix.SwapOnVSync(canvas)
# time.sleep(0.03) sleep(10)
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(run()) run()
-4
View File
@@ -1,4 +0,0 @@
import govee
govee_api = govee.GoveeApi(device_ip="172.16.0.15")
govee_api.send_scene("owABAgT/AGQMACr//+YAADb//8k=,o//QAAIDAwAAAAAAAAAAAAAAAI4=,MwUKdwAAAAAAAAAAAAAAAAAAAEs=")