Compare commits
96 Commits
f84b52543a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c1350ac01 | |||
| 7e265e7a2a | |||
| 0db43e3135 | |||
| 8d7bc08499 | |||
| bb3aa2e704 | |||
| 886e18d763 | |||
| e36994610f | |||
| 5cf53fbb19 | |||
| c8f28930e5 | |||
| 7fcd4df164 | |||
| 6560509eed | |||
| 51d4fb2656 | |||
| 0345fa1fee | |||
| fb310420e7 | |||
| 82f2970630 | |||
| fa7cad0b4e | |||
| a9fc06b5c8 | |||
| 245ef271c0 | |||
| dd644bc38c | |||
| 93c610902b | |||
| 1be8c704f8 | |||
| ab179df27c | |||
| 2e04da5aa6 | |||
| 8233bf4d1b | |||
| 43755aba7b | |||
| 3817286966 | |||
| ca5c8d6c1c | |||
| ab08c80d80 | |||
| 0661485c5e | |||
| a6957139e7 | |||
| 97ee9f5ccd | |||
| c18382f370 | |||
| 5ed0bdf643 | |||
| 26b842668f | |||
| 61f397076c | |||
| 95968cdcb6 | |||
| a711450445 | |||
|
a5a15de69b
|
|||
|
3359e89027
|
|||
|
043f44f8fa
|
|||
| 394ceec192 | |||
| 8097bb9a10 | |||
| 2ee6517162 | |||
| a4d79d4536 | |||
| 48c1db9ddb | |||
| a64f089054 | |||
| dcc42e1aa1 | |||
| 6f12614c36 | |||
| 525c70cf91 | |||
| 6bb1f74cf3 | |||
| 07a0bbab6e | |||
| d799459d50 | |||
| d7ee473c15 | |||
| d7b258bb51 | |||
| 0bfac21b92 | |||
| 386fdfc04a | |||
| 7cde8237a0 | |||
| 9c48a95ed3 | |||
| 0f8e7dce1f | |||
| 69f15bdfc1 | |||
| e33495e525 | |||
| a4093b019d | |||
| fcee870ec3 | |||
| 57aa924e7f | |||
| efbb6f5737 | |||
| 6ac11cb4a8 | |||
| 11a6abdf10 | |||
| e62630b7e3 | |||
| 886128c9c9 | |||
| db2540af6e | |||
| 8afd56c9c2 | |||
| 2dd786bded | |||
| 972cd94305 | |||
| a7ad381e3a | |||
| 198bf89356 | |||
| e23182f040 | |||
| f2fe63c944 | |||
| 83f67e2156 | |||
| 8dd43a85a6 | |||
| f0f8fdc01a | |||
|
ba47ae0952
|
|||
| 920e11acdf | |||
| 8aaeb8a393 | |||
| e5f4c07c16 | |||
| 2faa7b7982 | |||
| 43a22359df | |||
| 2f9fe7cf94 | |||
| 12a3909f24 | |||
| 1599df415b | |||
| 28db60ad86 | |||
| 9d85a6a76b | |||
| 9f81f497e0 | |||
| 6690acc346 | |||
| a8d1ea2b8d | |||
| dd9e4ba193 | |||
| 00fbcc1ce6 |
@@ -0,0 +1,3 @@
|
||||
GOVEE_API_KEY=
|
||||
GOVEE_DEVICE=
|
||||
GOVEE_SKU=
|
||||
+3
-1
@@ -1 +1,3 @@
|
||||
__pycache__
|
||||
__pycache__
|
||||
.env
|
||||
logos
|
||||
+25905
File diff suppressed because it is too large
Load Diff
+64553
File diff suppressed because it is too large
Load Diff
+119182
File diff suppressed because it is too large
Load Diff
+9
-4
@@ -7,12 +7,13 @@ LEAGUES = [
|
||||
("hockey", "nhl"),
|
||||
("football", "nfl"),
|
||||
("basketball", "nba"),
|
||||
("baseball", "mlb"),
|
||||
]
|
||||
|
||||
LOGO_DIR = "/home/alex/logos"
|
||||
LOGO_SIZE = (16, 16)
|
||||
|
||||
os.makedirs(LOGO_DIR, exist_ok=True)
|
||||
os.makedirs("./assets/logos", exist_ok=True)
|
||||
|
||||
|
||||
def download_logos(sport, league):
|
||||
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:
|
||||
print(f"No logo for {abbr}, skipping")
|
||||
continue
|
||||
if os.path.exists(os.path.join("./assets/logos", f"{league}_{abbr}.png")):
|
||||
print(f"Logo exists for {abbr}, skipping")
|
||||
continue
|
||||
|
||||
try:
|
||||
img_resp = requests.get(logo_url, timeout=10)
|
||||
@@ -40,14 +44,15 @@ def download_logos(sport, league):
|
||||
background = Image.new("RGB", LOGO_SIZE, (0, 0, 0))
|
||||
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)
|
||||
print(f"Saved {out_path}")
|
||||
except Exception as e:
|
||||
print(f"Error downloading {abbr}: {e}")
|
||||
|
||||
|
||||
for sport, league in LEAGUES:
|
||||
print(f"\nDownloading {league.upper()} logos...")
|
||||
download_logos(sport, league)
|
||||
|
||||
print("\nDone!")
|
||||
print("\nDone!")
|
||||
|
||||
@@ -1,100 +1,81 @@
|
||||
import asyncio
|
||||
import requests
|
||||
import random
|
||||
import string
|
||||
import json
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
GOVEE_LAN_PORT = 4003
|
||||
INTER_SEGMENT_DELAY = 0.1
|
||||
GOVEE_API_BASE_URL = "https://openapi.api.govee.com/router/api/v1/"
|
||||
|
||||
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:
|
||||
def __init__(self, device_ip: str, retries: int = 3, retry_delay: float = 0.05):
|
||||
self.device_ip = device_ip
|
||||
self.retries = retries
|
||||
self.retry_delay = retry_delay
|
||||
self._protocol: Optional[GoveeProtocol] = None
|
||||
key = ""
|
||||
headers = {}
|
||||
|
||||
async def connect(self):
|
||||
loop = asyncio.get_running_loop()
|
||||
_, self._protocol = await loop.create_datagram_endpoint(
|
||||
GoveeProtocol,
|
||||
remote_addr=(self.device_ip, GOVEE_LAN_PORT)
|
||||
)
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
self.headers = {"Govee-API-Key": key}
|
||||
|
||||
async def close(self):
|
||||
if self._protocol and self._protocol.transport:
|
||||
self._protocol.transport.close()
|
||||
def __get_random_string(self):
|
||||
characters = string.ascii_letters + string.digits
|
||||
result_str = "".join(random.choices(characters, k=4))
|
||||
return result_str
|
||||
|
||||
async def __aenter__(self):
|
||||
await self.connect()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *args):
|
||||
await self.close()
|
||||
|
||||
async def send_scene(self, scene_code: str):
|
||||
power_payload = {
|
||||
"msg": {
|
||||
"cmd": "turn",
|
||||
"data": {
|
||||
"value": 1 # 1 = on, 0 = off
|
||||
}
|
||||
}
|
||||
}
|
||||
self._send(power_payload)
|
||||
time.sleep(0.5)
|
||||
|
||||
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):
|
||||
def get_diy_scenes(self, sku, device):
|
||||
payload = {
|
||||
"msg": {
|
||||
"cmd": "colorwc",
|
||||
"data": {
|
||||
"color": {"r": r, "g": g, "b": b},
|
||||
"colorTemInKelvin": kelvin
|
||||
}
|
||||
}
|
||||
"requestId": self.__get_random_string(),
|
||||
"payload": {"sku": sku, "device": device},
|
||||
}
|
||||
await self._send(payload)
|
||||
|
||||
async def _send(self, payload: dict) -> Optional[dict]:
|
||||
if not self._protocol:
|
||||
raise RuntimeError("Not connected — use async with or call connect() first")
|
||||
res = requests.post(
|
||||
GOVEE_API_BASE_URL + "device/diy-scenes", json=payload, headers=self.headers
|
||||
)
|
||||
res.raise_for_status()
|
||||
|
||||
data = json.dumps(payload).encode()
|
||||
loop = asyncio.get_running_loop()
|
||||
print("[GOVEE] DIY scene fetch: " + json.dumps(res.json(), indent=4))
|
||||
|
||||
for attempt in range(self.retries):
|
||||
future: asyncio.Future = loop.create_future()
|
||||
self._protocol._pending = future
|
||||
self._protocol.transport.sendto(data)
|
||||
return res.ok
|
||||
|
||||
try:
|
||||
return await asyncio.wait_for(future, timeout=1.0)
|
||||
except asyncio.TimeoutError:
|
||||
if attempt < self.retries - 1:
|
||||
await asyncio.sleep(self.retry_delay)
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
print(f"Warning: no response after {self.retries} attempts")
|
||||
return None
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
res = requests.post(
|
||||
GOVEE_API_BASE_URL + "device/control", json=payload, headers=self.headers
|
||||
)
|
||||
res.raise_for_status()
|
||||
|
||||
print("[GOVEE] Set to original: " + json.dumps(res.json(), indent=4))
|
||||
|
||||
return res.ok
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,4 @@
|
||||
pygame
|
||||
requests
|
||||
python-dotenv
|
||||
pillow
|
||||
+260
-164
@@ -1,11 +1,24 @@
|
||||
import time
|
||||
import requests
|
||||
import os
|
||||
import govee
|
||||
import asyncio
|
||||
from time import sleep
|
||||
import math
|
||||
import pygame
|
||||
from enum import Enum
|
||||
from time import sleep, time
|
||||
from pathlib import Path
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
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 ---
|
||||
options = RGBMatrixOptions()
|
||||
@@ -13,7 +26,7 @@ options.rows = 32
|
||||
options.cols = 64
|
||||
options.chain_length = 4
|
||||
options.parallel = 1
|
||||
options.hardware_mapping = 'regular'
|
||||
options.hardware_mapping = "regular"
|
||||
options.gpio_slowdown = 5
|
||||
options.disable_hardware_pulsing = True
|
||||
options.brightness = 80
|
||||
@@ -21,46 +34,43 @@ options.brightness = 80
|
||||
matrix = RGBMatrix(options=options)
|
||||
canvas = matrix.CreateFrameCanvas()
|
||||
|
||||
# --- Font initialization ---
|
||||
font = graphics.Font()
|
||||
font.LoadFont("/usr/local/share/7x13.bdf")
|
||||
|
||||
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()
|
||||
try:
|
||||
font_big.LoadFont("/usr/local/share/9x18.bdf")
|
||||
except:
|
||||
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"
|
||||
font.LoadFont(os.path.join(ASSET_DIR, "fonts/7x13.bdf"))
|
||||
font_small.LoadFont(os.path.join(ASSET_DIR, "fonts/5x7.bdf"))
|
||||
font_big.LoadFont(os.path.join(ASSET_DIR, "fonts/9x18.bdf"))
|
||||
|
||||
# --- Logo cache ---
|
||||
logo_cache = {}
|
||||
|
||||
# --- Govee API ---
|
||||
govee_api = govee.GoveeApi(device_ip=GOVEE_IP)
|
||||
# --- Pre-built colors ---
|
||||
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):
|
||||
big_h = max(8, int(32 * text_scale))
|
||||
big_img = Image.new("RGB", (1024, 128), bg_color)
|
||||
big_draw = ImageDraw.Draw(big_img)
|
||||
|
||||
# rpi specific, fall back to default font if not existing
|
||||
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:
|
||||
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)
|
||||
|
||||
# paste sabres logo on left and right
|
||||
# Replace the logo paste section with this
|
||||
logo_path = os.path.join(LOGO_DIR, "nhl_BUF.png")
|
||||
if os.path.exists(logo_path):
|
||||
try:
|
||||
@@ -108,65 +117,72 @@ def render_goal_frame(text, text_scale, bg_color, text_color):
|
||||
|
||||
return scaled
|
||||
|
||||
def play_goal_celebration(text, color1, color2):
|
||||
global canvas
|
||||
|
||||
# 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]
|
||||
for _ in range(5):
|
||||
for i, scale in enumerate(zoom_steps):
|
||||
bg = color1 if i % 2 == 0 else color2
|
||||
fg = color2 if i % 2 == 0 else color1
|
||||
frame = render_goal_frame(text, scale, bg, fg)
|
||||
canvas.Clear()
|
||||
draw_pil_image(canvas, frame)
|
||||
canvas = matrix.SwapOnVSync(canvas)
|
||||
sleep(0.05)
|
||||
|
||||
# Phase 2: rapid flashing at full size
|
||||
for i in range(10):
|
||||
bg = color1 if i % 2 == 0 else color2
|
||||
fg = color2 if i % 2 == 0 else color1
|
||||
frame = render_goal_frame(text, 1.0, bg, fg)
|
||||
canvas.Clear()
|
||||
draw_pil_image(canvas, frame)
|
||||
canvas = matrix.SwapOnVSync(canvas)
|
||||
sleep(0.12)
|
||||
|
||||
# Phase 3: zoom back out and fade to white flash
|
||||
zoom_out = [1.0, 1.1, 1.2, 1.3, 1.4]
|
||||
for i, scale in enumerate(zoom_out):
|
||||
bg = color2 if i % 2 == 0 else color1
|
||||
fg = color1 if i % 2 == 0 else color2
|
||||
frame = render_goal_frame(text, scale, bg, fg)
|
||||
canvas.Clear()
|
||||
draw_pil_image(canvas, frame)
|
||||
canvas = matrix.SwapOnVSync(canvas)
|
||||
sleep(0.08)
|
||||
|
||||
# Phase 4: white flash to end
|
||||
for _ in range(3):
|
||||
canvas.Clear()
|
||||
frame = render_goal_frame(
|
||||
text, 1.0, Colors.SABRES_GOLD.value, Colors.SABRES_BLUE.value
|
||||
)
|
||||
draw_pil_image(canvas, frame)
|
||||
canvas = matrix.SwapOnVSync(canvas)
|
||||
sleep(0.1)
|
||||
|
||||
# clear board
|
||||
canvas = canvas.Clear()
|
||||
canvas = matrix.SwapOnVSync(canvas)
|
||||
|
||||
# stop music if playing
|
||||
pygame.mixer.music.stop()
|
||||
|
||||
# Hold for a moment then return to scoreboard
|
||||
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) # swap r and g for GRB panels
|
||||
|
||||
def play_goal_celebration():
|
||||
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
|
||||
zoom_steps = [0.1, 0.2, 0.35, 0.5, 0.65, 0.8, 0.95, 1.1, 1.0]
|
||||
for i, scale in enumerate(zoom_steps):
|
||||
bg = BLUE if i % 2 == 0 else GOLD
|
||||
fg = GOLD if i % 2 == 0 else BLUE
|
||||
frame = render_goal_frame(TEXT, scale, bg, fg)
|
||||
canvas.Clear()
|
||||
draw_pil_image(canvas, frame)
|
||||
canvas = matrix.SwapOnVSync(canvas)
|
||||
time.sleep(0.05)
|
||||
|
||||
# Phase 2: rapid flashing at full size
|
||||
for i in range(10):
|
||||
bg = BLUE if i % 2 == 0 else GOLD
|
||||
fg = GOLD if i % 2 == 0 else BLUE
|
||||
frame = render_goal_frame(TEXT, 1.0, bg, fg)
|
||||
canvas.Clear()
|
||||
draw_pil_image(canvas, frame)
|
||||
canvas = matrix.SwapOnVSync(canvas)
|
||||
time.sleep(0.12)
|
||||
|
||||
# Phase 3: zoom back out and fade to white flash
|
||||
zoom_out = [1.0, 1.1, 1.2, 1.3, 1.4]
|
||||
for i, scale in enumerate(zoom_out):
|
||||
bg = GOLD if i % 2 == 0 else BLUE
|
||||
fg = BLUE if i % 2 == 0 else GOLD
|
||||
frame = render_goal_frame(TEXT, scale, bg, fg)
|
||||
canvas.Clear()
|
||||
draw_pil_image(canvas, frame)
|
||||
canvas = matrix.SwapOnVSync(canvas)
|
||||
time.sleep(0.08)
|
||||
|
||||
# Phase 4: white flash to end
|
||||
for _ in range(3):
|
||||
canvas.Clear()
|
||||
frame = render_goal_frame(TEXT, 1.0, WHITE, BLUE)
|
||||
draw_pil_image(canvas, frame)
|
||||
canvas = matrix.SwapOnVSync(canvas)
|
||||
time.sleep(0.1)
|
||||
canvas.Clear()
|
||||
canvas = matrix.SwapOnVSync(canvas)
|
||||
time.sleep(0.08)
|
||||
|
||||
# Hold for a moment then return to scoreboard
|
||||
time.sleep(0.5)
|
||||
canvas.SetPixel(x, y, b, g, r) # bgr panels
|
||||
|
||||
def load_logo(league, abbr):
|
||||
key = f"{league}_{abbr}"
|
||||
@@ -194,7 +210,7 @@ def draw_logo(canvas, img, x, y):
|
||||
for px in range(img.width):
|
||||
for py in range(img.height):
|
||||
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 ---
|
||||
def get_scores(sport, league):
|
||||
@@ -209,55 +225,32 @@ def get_scores(sport, league):
|
||||
home = next(t for t in teams if t["homeAway"] == "home")
|
||||
away = next(t for t in teams if t["homeAway"] == "away")
|
||||
status = event["status"]["type"]["shortDetail"]
|
||||
games.append({
|
||||
"league": league,
|
||||
"away": away["team"]["abbreviation"].upper(),
|
||||
"away_score": away["score"],
|
||||
"home": home["team"]["abbreviation"].upper(),
|
||||
"home_score": home["score"],
|
||||
"status": status,
|
||||
"id": event["id"],
|
||||
})
|
||||
games.append(
|
||||
{
|
||||
"league": league,
|
||||
"away": away["team"]["abbreviation"].upper(),
|
||||
"away_score": away["score"],
|
||||
"home": home["team"]["abbreviation"].upper(),
|
||||
"home_score": home["score"],
|
||||
"status": status,
|
||||
"id": event["id"],
|
||||
}
|
||||
)
|
||||
return games
|
||||
except Exception as e:
|
||||
print(f"Fetch error ({league}): {e}")
|
||||
return []
|
||||
|
||||
def get_all_scores():
|
||||
print('fetching game scores from espn')
|
||||
games = []
|
||||
games += get_scores("hockey", "nhl")
|
||||
games += get_scores("football", "nfl")
|
||||
games += get_scores("basketball", "nba")
|
||||
games += get_scores("baseball", "mlb")
|
||||
return games
|
||||
|
||||
def sabres_scored(games, prev_scores):
|
||||
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 ---
|
||||
# --- Game drawing ---
|
||||
def draw_all_games(canvas, games, start_index):
|
||||
for i in range(4):
|
||||
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, home_logo, offset + 0, 16)
|
||||
|
||||
graphics.DrawText(canvas, font_small, offset + 18, 11, white, game["away"])
|
||||
graphics.DrawText(canvas, font_small, offset + 18, 27, white, game["home"])
|
||||
graphics.DrawText(
|
||||
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(canvas, font, offset + 40, 29, yellow, str(game["home_score"]))
|
||||
graphics.DrawText(
|
||||
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:
|
||||
for row in range(32):
|
||||
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 ---
|
||||
async def run():
|
||||
def run():
|
||||
global canvas
|
||||
games = []
|
||||
prev_scores = {}
|
||||
last_fetch = 0
|
||||
current_page = 0
|
||||
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:
|
||||
async with govee_api:
|
||||
await govee_api.send_scene(GOVEE_AWS)
|
||||
play_goal_celebration()
|
||||
await govee_api.set_color(255,0,0)
|
||||
now = time()
|
||||
|
||||
# while True:
|
||||
# now = time.time()
|
||||
if now - last_fetch > 30 or len(games) <= 0:
|
||||
games = get_all_scores()
|
||||
last_fetch = now
|
||||
|
||||
# if now - last_fetch > 30:
|
||||
# new_games = get_all_scores()
|
||||
if games:
|
||||
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
|
||||
# if prev_scores and sabres_scored(new_games, prev_scores):
|
||||
# play_goal_celebration()
|
||||
# get new preferred games
|
||||
for game in games:
|
||||
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
|
||||
# for game in new_games:
|
||||
# gid = game["id"]
|
||||
# try:
|
||||
# prev_scores[gid] = (int(game["away_score"]), int(game["home_score"]))
|
||||
# except ValueError:
|
||||
# pass
|
||||
print(preferred_games)
|
||||
if now - last_switch > page_display_time:
|
||||
last_switch = now
|
||||
|
||||
# games = new_games
|
||||
# last_fetch = now
|
||||
# if not games:
|
||||
# current_page = 0
|
||||
if show_preferred and len(preferred_games) > 0:
|
||||
single_preferred_game = next(
|
||||
(g for g in games if g['id'] == preferred_games[current_preferred_game]), None
|
||||
)
|
||||
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_page = (current_page + 4) % max(len(games), 1)
|
||||
# last_switch = now
|
||||
current_preferred_game += 1
|
||||
if current_preferred_game >= len(preferred_games):
|
||||
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:
|
||||
# draw_all_games(canvas, games, current_page)
|
||||
# else:
|
||||
# graphics.DrawText(canvas, font, 10, 22, red, "No games today")
|
||||
# no games available, just draw placeholder
|
||||
graphics.DrawText(canvas, font, 10, 22, graphics.Color(*Colors.RED.value), "No games today")
|
||||
|
||||
# canvas = matrix.SwapOnVSync(canvas)
|
||||
# time.sleep(0.03)
|
||||
canvas = matrix.SwapOnVSync(canvas)
|
||||
sleep(10)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(run())
|
||||
run()
|
||||
|
||||
Reference in New Issue
Block a user