house cleaning

This commit is contained in:
2026-05-03 03:27:57 -04:00
parent 920e11acdf
commit ba47ae0952
10 changed files with 209809 additions and 170 deletions
+3 -1
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
+6 -3
View File
@@ -7,12 +7,12 @@ 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 +28,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,7 +43,7 @@ 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:
+24 -21
View File
@@ -6,37 +6,37 @@ import json
GOVEE_API_BASE_URL = "https://openapi.api.govee.com/router/api/v1/"
class GoveeApi:
device_ip = ""
key = ""
headers = {}
def __init__(self, key):
self.key = key
self.headers = {
'Govee-API-Key': key
}
def get_random_string(self):
# Choose from all lowercase, uppercase letters, and digits
def __get_random_string(self):
characters = string.ascii_letters + string.digits
result_str = ''.join(random.choices(characters, k=4))
return result_str
def get_diy_scenes(self, sku, device):
headers = {
'Govee-API-Key': self.key
}
payload = {
"requestId": self.get_random_string(),
"requestId": self.__get_random_string(),
"payload": {
"sku": sku,
"device": device
}
}
res = requests.post(GOVEE_API_BASE_URL + 'device/diy-scenes', json=payload, headers=headers)
res = requests.post(GOVEE_API_BASE_URL + 'device/diy-scenes', json=payload, headers=self.headers)
res.raise_for_status()
print(json.dumps(res.json(), indent=4))
print("[GOVEE] DIY scene fetch: " + json.dumps(res.json(), indent=4))
return res.ok
def set_diy_scene(self, sku, device):
headers = {
'Govee-API-Key': self.key
}
payload = {
'requestId': self.get_random_string(),
'payload': {
@@ -50,15 +50,14 @@ class GoveeApi:
}
}
print(json.dumps(payload, indent=4))
req = requests.post(GOVEE_API_BASE_URL + 'device/control', headers=headers, json=payload)
req.raise_for_status()
print(req.json())
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):
headers = {
'Govee-API-Key': self.key
}
payload = {
'requestId': self.get_random_string(),
'payload': {
@@ -72,5 +71,9 @@ class GoveeApi:
}
}
req = requests.post(GOVEE_API_BASE_URL + 'device/control', json=payload, headers=headers)
req.raise_for_status()
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
+15
View File
@@ -0,0 +1,15 @@
# scoreboard
Scoreboard script for showing active sports I like.
# Setup
Must have _rpi-rgb-led-matrix_ setup prior to use & installed via pip. Run `python download_logos.py` to download logos to reflect, also configure the values in the ENV example.
# Running
Just run
```bash
python scoreboard.py
```
+4
View File
@@ -0,0 +1,4 @@
pygame
requests
python-dotenv
pillow
+117 -145
View File
@@ -1,11 +1,19 @@
import time
import requests
import os
import govee
import pygame
from time import sleep
from enum import Enum
from time import sleep, time
from PIL import Image, ImageDraw, ImageFont
from rgbmatrix import RGBMatrix, RGBMatrixOptions, graphics
from dotenv import load_dotenv
# --- Load environment vars ---
load_dotenv()
# --- Default vars ---
ASSET_DIR = "./assets"
LOGO_DIR = os.path.join(ASSET_DIR, 'logos')
# --- Matrix config ---
options = RGBMatrixOptions()
@@ -21,49 +29,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")
font_big = graphics.Font()
font.LoadFont(os.path.join(ASSET_DIR, 'fonts/7x13.bdf'))
font_small.LoadFont(os.path.join(ASSET_DIR, 'fonts/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")
font_big.LoadFont(os.path.join(ASSET_DIR, 'fonts/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"
# --- Logo cache ---
logo_cache = {}
# --- 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 ---
govee_api = govee.GoveeApi(key="")
govee_api = govee.GoveeApi(key=os.environ['GOVEE_API_KEY'])
# --- PyGame Audio ---
pygame.mixer.init()
def play_goal_horn():
pygame.mixer.music.load("/usr/local/share/horn.mp3")
pygame.mixer.music.play()
# --- 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)
except:
@@ -80,7 +82,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:
@@ -113,62 +114,70 @@ 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, Colors.SABRES_BLUE)
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)
# 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}"
@@ -196,7 +205,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, b, g, r) # bgr panels
# --- Fetch scores ---
def get_scores(sport, league):
@@ -230,35 +239,9 @@ def get_all_scores():
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 ---
def draw_all_games(canvas, games, start_index):
for i in range(4):
@@ -273,11 +256,11 @@ 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, Colors.WHITE, game["away"])
graphics.DrawText(canvas, font_small, offset + 18, 27, Colors.WHITE, 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, Colors.YELLOW, str(game["away_score"]))
graphics.DrawText(canvas, font, offset + 40, 29, Colors.YELLOW, str(game["home_score"]))
if i < 3:
for row in range(32):
@@ -291,51 +274,40 @@ def run():
last_fetch = 0
current_page = 0
page_display_time = 8
last_switch = time.time()
last_switch = time()
while True:
play_goal_horn()
govee_api.set_diy_scene(GOVEE_SKU, GOVEE_DEVICE)
play_goal_celebration()
now = time.time()
sleep(60)
if now - last_fetch > 30:
new_games = get_all_scores()
# while True:
# now = time.time()
# 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
# if now - last_fetch > 30:
# new_games = get_all_scores()
games = new_games
last_fetch = now
if not games:
current_page = 0
# # check for sabres goal before updating prev_scores
# if prev_scores and sabres_scored(new_games, prev_scores):
# play_goal_celebration()
if games and now - last_switch > page_display_time:
current_page = (current_page + 4) % max(len(games), 1)
last_switch = now
# # 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
canvas.Clear()
# games = new_games
# last_fetch = now
# if not games:
# current_page = 0
if games:
draw_all_games(canvas, games, current_page)
else:
graphics.DrawText(canvas, font, 10, 22, Colors.RED, "No games today")
# if games and now - last_switch > page_display_time:
# current_page = (current_page + 4) % max(len(games), 1)
# last_switch = now
# canvas.Clear()
# if games:
# draw_all_games(canvas, games, current_page)
# else:
# graphics.DrawText(canvas, font, 10, 22, red, "No games today")
# canvas = matrix.SwapOnVSync(canvas)
# time.sleep(0.03)
canvas = matrix.SwapOnVSync(canvas)
sleep(0.03)
if __name__ == "__main__":
run()