format with ruff

This commit is contained in:
2026-05-03 14:25:53 -04:00
parent 11a6abdf10
commit 6ac11cb4a8
3 changed files with 161 additions and 115 deletions
+3 -1
View File
@@ -7,13 +7,14 @@ LEAGUES = [
("hockey", "nhl"), ("hockey", "nhl"),
("football", "nfl"), ("football", "nfl"),
("basketball", "nba"), ("basketball", "nba"),
("baseball", "mlb") ("baseball", "mlb"),
] ]
LOGO_SIZE = (16, 16) LOGO_SIZE = (16, 16)
os.makedirs("./assets/logos", 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"
resp = requests.get(url, timeout=10) resp = requests.get(url, timeout=10)
@@ -49,6 +50,7 @@ def download_logos(sport, league):
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)
+33 -31
View File
@@ -5,31 +5,29 @@ import json
GOVEE_API_BASE_URL = "https://openapi.api.govee.com/router/api/v1/" GOVEE_API_BASE_URL = "https://openapi.api.govee.com/router/api/v1/"
class GoveeApi: class GoveeApi:
key = "" key = ""
headers = {} headers = {}
def __init__(self, key): def __init__(self, key):
self.key = key self.key = key
self.headers = { self.headers = {"Govee-API-Key": key}
'Govee-API-Key': key
}
def __get_random_string(self): def __get_random_string(self):
characters = string.ascii_letters + string.digits characters = string.ascii_letters + string.digits
result_str = ''.join(random.choices(characters, k=4)) result_str = "".join(random.choices(characters, k=4))
return result_str return result_str
def get_diy_scenes(self, sku, device): def get_diy_scenes(self, sku, device):
payload = { payload = {
"requestId": self.__get_random_string(), "requestId": self.__get_random_string(),
"payload": { "payload": {"sku": sku, "device": device},
"sku": sku,
"device": device
}
} }
res = requests.post(GOVEE_API_BASE_URL + 'device/diy-scenes', json=payload, headers=self.headers) res = requests.post(
GOVEE_API_BASE_URL + "device/diy-scenes", json=payload, headers=self.headers
)
res.raise_for_status() res.raise_for_status()
print("[GOVEE] DIY scene fetch: " + json.dumps(res.json(), indent=4)) print("[GOVEE] DIY scene fetch: " + json.dumps(res.json(), indent=4))
@@ -38,19 +36,21 @@ class GoveeApi:
def set_diy_scene(self, sku, device): def set_diy_scene(self, sku, device):
payload = { payload = {
'requestId': self.__get_random_string(), "requestId": self.__get_random_string(),
'payload': { "payload": {
'sku': sku, "sku": sku,
'device': device, "device": device,
'capability': { "capability": {
'type': 'device.capabilities.dynamic_scene', "type": "device.capabilities.dynamic_scene",
'instance': 'diyScene', "instance": "diyScene",
'value': 22757907 "value": 22757907,
} },
} },
} }
res = requests.post(GOVEE_API_BASE_URL + 'device/control', headers=self.headers, json=payload) res = requests.post(
GOVEE_API_BASE_URL + "device/control", headers=self.headers, json=payload
)
res.raise_for_status() res.raise_for_status()
print("[GOVEE] Set DIY scene: " + json.dumps(res.json(), indent=4)) print("[GOVEE] Set DIY scene: " + json.dumps(res.json(), indent=4))
@@ -59,19 +59,21 @@ class GoveeApi:
def set_to_original_color(self, sku, device): def set_to_original_color(self, sku, device):
payload = { payload = {
'requestId': self.__get_random_string(), "requestId": self.__get_random_string(),
'payload': { "payload": {
'sku': sku, "sku": sku,
'device': device, "device": device,
'capability': { "capability": {
'type': 'devices.capabilities.color_setting', "type": "devices.capabilities.color_setting",
'instance': 'colorRgb', "instance": "colorRgb",
'value': 16711680 "value": 16711680,
} },
} },
} }
res = requests.post(GOVEE_API_BASE_URL + 'device/control', json=payload, headers=self.headers) res = requests.post(
GOVEE_API_BASE_URL + "device/control", json=payload, headers=self.headers
)
res.raise_for_status() res.raise_for_status()
print("[GOVEE] Set to original: " + json.dumps(res.json(), indent=4)) print("[GOVEE] Set to original: " + json.dumps(res.json(), indent=4))
+87 -45
View File
@@ -13,7 +13,7 @@ load_dotenv()
# --- Default vars --- # --- Default vars ---
ASSET_DIR = "./assets" ASSET_DIR = "./assets"
LOGO_DIR = os.path.join(ASSET_DIR, 'logos') LOGO_DIR = os.path.join(ASSET_DIR, "logos")
# --- Matrix config --- # --- Matrix config ---
options = RGBMatrixOptions() options = RGBMatrixOptions()
@@ -21,7 +21,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
@@ -33,18 +33,19 @@ canvas = matrix.CreateFrameCanvas()
font = graphics.Font() font = graphics.Font()
font_small = graphics.Font() font_small = graphics.Font()
font_big = graphics.Font() font_big = graphics.Font()
font.LoadFont(os.path.join(ASSET_DIR, 'fonts/7x13.bdf')) font.LoadFont(os.path.join(ASSET_DIR, "fonts/7x13.bdf"))
font_small.LoadFont(os.path.join(ASSET_DIR, 'fonts/5x7.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 # try to load a big font for GOAL!, fall back to regular if not available
try: try:
font_big.LoadFont(os.path.join(ASSET_DIR, 'fonts/9x18.bdf')) font_big.LoadFont(os.path.join(ASSET_DIR, "fonts/9x18.bdf"))
except: except:
font_big = font font_big = font
# --- Logo cache --- # --- Logo cache ---
logo_cache = {} logo_cache = {}
# --- Pre-built colors --- # --- Pre-built colors ---
class Colors(Enum): class Colors(Enum):
WHITE = (255, 255, 255) WHITE = (255, 255, 255)
@@ -53,12 +54,14 @@ class Colors(Enum):
SABRES_BLUE = (0, 135, 48) SABRES_BLUE = (0, 135, 48)
SABRES_GOLD = (252, 20, 210) SABRES_GOLD = (252, 20, 210)
# --- Govee API --- # --- Govee API ---
govee_api = govee.GoveeApi(key=os.environ['GOVEE_API_KEY']) govee_api = govee.GoveeApi(key=os.environ["GOVEE_API_KEY"])
# --- PyGame Audio --- # --- PyGame Audio ---
pygame.mixer.init() pygame.mixer.init()
# --- Goal celebrations --- # --- 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))
@@ -67,7 +70,9 @@ def render_goal_frame(text, text_scale, bg_color, text_color):
# rpi specific, fall back to default font if not existing # 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()
@@ -114,6 +119,7 @@ def render_goal_frame(text, text_scale, bg_color, text_color):
return scaled return scaled
def play_goal_celebration(text, color1, color2): def play_goal_celebration(text, color1, color2):
global canvas global canvas
@@ -153,7 +159,9 @@ def play_goal_celebration(text, color1, color2):
# 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, Colors.SABRES_GOLD.value, Colors.SABRES_BLUE.value) 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)
sleep(0.1) sleep(0.1)
@@ -168,10 +176,12 @@ def play_goal_celebration(text, color1, color2):
# Hold for a moment then return to scoreboard # Hold for a moment then return to scoreboard
sleep(0.5) sleep(0.5)
def play_audio(filename): def play_audio(filename):
pygame.mixer.music.load(os.path.join(ASSET_DIR, filename)) pygame.mixer.music.load(os.path.join(ASSET_DIR, filename))
pygame.mixer.music.play() pygame.mixer.music.play()
# --- Utilities --- # --- Utilities ---
def draw_pil_image(canvas, img): def draw_pil_image(canvas, img):
for x in range(img.width): for x in range(img.width):
@@ -179,6 +189,7 @@ def draw_pil_image(canvas, img):
r, g, b = img.getpixel((x, y)) r, g, b = img.getpixel((x, y))
canvas.SetPixel(x, y, b, g, r) # bgr panels 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}"
if key in logo_cache: if key in logo_cache:
@@ -199,6 +210,7 @@ def load_logo(league, abbr):
logo_cache[key] = None logo_cache[key] = None
return None return None
def draw_logo(canvas, img, x, y): def draw_logo(canvas, img, x, y):
if img is None: if img is None:
return return
@@ -207,6 +219,7 @@ def draw_logo(canvas, img, x, y):
r, g, b = img.getpixel((px, py)) r, g, b = img.getpixel((px, py))
canvas.SetPixel(x + px, y + py, r, g, b) # bgr panels canvas.SetPixel(x + px, y + py, r, g, b) # bgr panels
# --- Fetch scores --- # --- Fetch scores ---
def get_scores(sport, league): def get_scores(sport, league):
url = f"https://site.api.espn.com/apis/site/v2/sports/{sport}/{league}/scoreboard" url = f"https://site.api.espn.com/apis/site/v2/sports/{sport}/{league}/scoreboard"
@@ -220,7 +233,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"],
@@ -228,12 +242,14 @@ 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():
games = [] games = []
games += get_scores("hockey", "nhl") games += get_scores("hockey", "nhl")
@@ -242,6 +258,7 @@ def get_all_scores():
games += get_scores("baseball", "mlb") games += get_scores("baseball", "mlb")
return games return games
# --- Draw all games across all panels --- # --- 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):
@@ -256,16 +273,45 @@ 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, graphics.Color(255,255,255), game["away"]) graphics.DrawText(
graphics.DrawText(canvas, font_small, offset + 18, 27, graphics.Color(255,255,255), game["home"]) canvas,
font_small,
offset + 18,
11,
graphics.Color(255, 255, 255),
game["away"],
)
graphics.DrawText(
canvas,
font_small,
offset + 18,
27,
graphics.Color(255, 255, 255),
game["home"],
)
graphics.DrawText(canvas, font, offset + 40, 13, graphics.Color(255,255,255), str(game["away_score"])) graphics.DrawText(
graphics.DrawText(canvas, font, offset + 40, 29, graphics.Color(255,255,255), str(game["home_score"])) canvas,
font,
offset + 40,
13,
graphics.Color(255, 255, 255),
str(game["away_score"]),
)
graphics.DrawText(
canvas,
font,
offset + 40,
29,
graphics.Color(255, 255, 255),
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)
# --- Main loop --- # --- Main loop ---
def run(): def run():
global canvas global canvas
@@ -282,47 +328,43 @@ def run():
print(game.status, game.home, game.away) print(game.status, game.home, game.away)
while True: while True:
# now = time()
# if now - last_fetch > 30:
# new_games = get_all_scores()
# # update prev_scores
# for game in new_games:
# try:
# prev_scores[gid] = (int(game["away_score"]), int(game["home_score"]))
# except ValueError:
# pass
# games = new_games
# last_fetch = now
# if not games:
# current_page = 0
# if games and now - last_switch > page_display_time:
# current_page = (current_page + 4) % max(len(games), 1)
# last_switch = now
# if games:
# draw_all_games(canvas, games, current_page)
# else:
# graphics.DrawText(canvas, font, 10, 22, graphics.Color(Colors.RED), "No games today")
# canvas = matrix.SwapOnVSync(canvas)
# sleep(0.03)
now = time() now = time()
if now - last_fetch > 30:
new_games = get_all_scores()
# update prev_scores
for game in new_games:
gid = game["id"] gid = game["id"]
try:
prev_scores[gid] = (
int(game["away_score"]),
int(game["home_score"]),
)
except ValueError: except ValueError:
pass
games = new_games
last_fetch = now
if not games:
current_page = 0
if games and now - last_switch > page_display_time:
current_page = (current_page + 4) % max(len(games), 1) current_page = (current_page + 4) % max(len(games), 1)
last_switch = now
canvas.Clear()
if games: if games:
draw_all_games(canvas, games, current_page) draw_all_games(canvas, games, current_page)
else: else:
graphics.DrawText(canvas, font, 10, 22, graphics.Color(Colors.RED), "No games today") graphics.DrawText(
canvas, font, 10, 22, graphics.Color(Colors.RED), "No games today"
)
canvas = matrix.SwapOnVSync(canvas) canvas = matrix.SwapOnVSync(canvas)
sleep(0.03) sleep(0.03)
if __name__ == "__main__": if __name__ == "__main__":
run() run()