import requests from PIL import Image from time import time from rgbmatrix import graphics from vars import PANEL_HEIGHT, PANEL_WIDTH, GAME_WIDTH, DIVIDER_COLOR, Colors, font, font_small games = [] last_fetch = 0 preferred_games = [] preferred_teams = [] scroll_x = 0 scroll_speed = 1 frames_per_tick = 2 tick = 0 times_scrolled = 0 virtual_canvas = None virtual_dirty = True def get_scores(sport, league): url = f"https://site.api.espn.com/apis/site/v2/sports/{sport}/{league}/scoreboard" try: resp = requests.get(url, timeout=5) resp.raise_for_status() result = [] for event in resp.json().get("events", []): comp = event["competitions"][0] teams = comp["competitors"] 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"] result.append({ "league": league, "venue": comp["venue"]["fullName"], "away": away["team"]["abbreviation"].upper(), "away_score": away["score"], "home": home["team"]["abbreviation"].upper(), "home_score": home["score"], "status": status, "id": event["id"], }) return result except Exception as e: print(f"Fetch error ({league}): {e}") return [] def get_all_scores(): print("fetching game scores from espn") result = [] result += get_scores("hockey", "nhl") result += get_scores("football", "nfl") result += get_scores("basketball", "nba") result += get_scores("baseball", "mlb") return result def ordered_games(): preferred_ids = set(preferred_games) preferred = [g for g in games if g["id"] in preferred_ids] return preferred def update_preferred(): for gid in list(preferred_games): game = next((g for g in games if g["id"] == gid), None) if game is None or "Final" in game["status"]: preferred_games.remove(gid) # add new matching games for game in games: if (game["away"], game["league"]) in preferred_teams or \ (game["home"], game["league"]) in preferred_teams: preferred_games.append(game["id"]) def draw_text_overlay(canvas, ordered, scroll_x): """Draw all game text onto the rgbmatrix canvas accounting for scroll offset.""" total_width = GAME_WIDTH * len(ordered) for i, game in enumerate(ordered): base_x = (i * GAME_WIDTH) - scroll_x # draw twice to handle the wrap-around copy for wrap in [0, total_width]: x = base_x + wrap # cull slots fully off screen if x + GAME_WIDTH < 0 or x >= PANEL_WIDTH: continue graphics.DrawText(canvas, font_small, x + 18, 11, Colors.RED.value, game["away"]) graphics.DrawText(canvas, font_small, x + 18, 27, Colors.WHITE.value, game["home"]) graphics.DrawText(canvas, font, x + 40, 13, Colors.WHITE.value, str(game["away_score"])) graphics.DrawText(canvas, font, x + 40, 29, Colors.WHITE.value, str(game["home_score"])) # status line — only on preferred games (they get a wider single-game view) # if game["id"] in set(_preferred_games): # graphics.DrawText(canvas, font_small, x + 18, 20, # _rbg(Colors.YELLOW.value), "\n".join(game["status"].split("-"))) # if the time is shown it should be split between lines # if not, just display the status if "AM" in game["status"] or "PM" in game["status"]: game_status_split = game["status"].split("-") date = game_status_split[0].strip() time = game_status_split[1].strip() graphics.DrawText(canvas, font_small, x + 60, 10, Colors.YELLOW.value, date) graphics.DrawText(canvas, font_small, x + 60, 20, Colors.YELLOW.value, time) graphics.DrawText(canvas, font_small, x + 60, 30, Colors.YELLOW.value, game["venue"]) else: graphics.DrawText(canvas, font_small, x + 65, 20, Colors.YELLOW.value, game["status"]) def render_game(matrix, img, game, x_offset): league = game["league"] away_logo = matrix.load_logo_to_image(league, game["away"]) home_logo = matrix.load_logo_to_image(league, game["home"]) if away_logo: img.paste(away_logo.resize((14, 14)), (x_offset, 0)) if home_logo: img.paste(home_logo.resize((14, 14)), (x_offset, 16)) # divider on right edge (except last slot handled by wrapping) for row in range(PANEL_HEIGHT): img.putpixel((x_offset + GAME_WIDTH - 1, row), DIVIDER_COLOR) def build_canvas(matrix): ordered = ordered_games() if not ordered: return None total_games = len(ordered) total_width = GAME_WIDTH * (total_games + 4) img = Image.new("RGB", (total_width, PANEL_HEIGHT), (0,0,0)) for i, game in enumerate(ordered * 2): if i >= total_games + 4: break render_game(matrix, img, game, i * GAME_WIDTH) return img, total_games def blit_slice(canvas, pil_img, x_offset): total_width = pil_img.width for x in range(PANEL_WIDTH): src_x = (x_offset + x) % total_width for y in range(PANEL_HEIGHT): r, g, b = pil_img.getpixel((src_x, y)) canvas.SetPixel(x, y, r, b, g) def draw_frame(matrix): global games, last_fetch, virtual_canvas, virtual_dirty, scroll_x, tick now = time() if now - last_fetch > 30 or not games: games = get_all_scores() last_fetch = now update_preferred() virtual_dirty = True if not games: matrix.canvas.Clear() graphics.DrawText(matrix.canvas, font, 10, 22, Colors.RED.value, "No games today") return matrix.canvas # rebuild virtual canvas if data changed if virtual_dirty or virtual_canvas is None: result = build_canvas() if result: virtual_canvas, total_games = result virtual_dirty = False scroll_x = 0 ordered = ordered_games() total_scroll_width = GAME_WIDTH * len(ordered) matrix.canvas.Clear() # blit the PIL image slice (logos + dividers + backgrounds) if virtual_canvas: blit_slice(matrix.canvas, virtual_canvas, scroll_x) # draw text on top via rgbmatrix (handles fonts correctly) draw_text_overlay(matrix.canvas, ordered, scroll_x) # advance scroll every N ticks _tick += 1 if _tick >= frames_per_tick: _tick = 0 next_x = scroll_x + scroll_speed if next_x >= total_scroll_width: matrix.next_slide("fantasy") scroll_x = next_x % total_scroll_width return matrix.canvas