mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-11 21:33:00 +00:00
refactor(nhl): Collect and cycle through all relevant favorite game statuses (live, upcoming, final)
This commit is contained in:
@@ -5,6 +5,7 @@ from pathlib import Path
|
|||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from typing import List, Dict, Optional
|
||||||
try:
|
try:
|
||||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -325,7 +326,7 @@ def create_scorebug_image(game_details):
|
|||||||
try:
|
try:
|
||||||
away_logo = Image.open(game_details["away_logo_path"]).convert("RGBA")
|
away_logo = Image.open(game_details["away_logo_path"]).convert("RGBA")
|
||||||
away_logo.thumbnail(logo_size, Image.Resampling.LANCZOS)
|
away_logo.thumbnail(logo_size, Image.Resampling.LANCZOS)
|
||||||
img.paste(away_logo, away_logo_pos, away_logo) # Use logo as mask for transparency
|
img.paste(away_logo, away_logo_pos, mask=away_logo)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error loading/pasting away logo {game_details['away_logo_path']}: {e}")
|
logging.error(f"Error loading/pasting away logo {game_details['away_logo_path']}: {e}")
|
||||||
# Draw placeholder text if logo fails
|
# Draw placeholder text if logo fails
|
||||||
@@ -341,7 +342,7 @@ def create_scorebug_image(game_details):
|
|||||||
try:
|
try:
|
||||||
home_logo = Image.open(game_details["home_logo_path"]).convert("RGBA")
|
home_logo = Image.open(game_details["home_logo_path"]).convert("RGBA")
|
||||||
home_logo.thumbnail(logo_size, Image.Resampling.LANCZOS)
|
home_logo.thumbnail(logo_size, Image.Resampling.LANCZOS)
|
||||||
img.paste(home_logo, home_logo_pos, home_logo)
|
img.paste(home_logo, home_logo_pos, mask=home_logo)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error loading/pasting home logo {game_details['home_logo_path']}: {e}")
|
logging.error(f"Error loading/pasting home logo {game_details['home_logo_path']}: {e}")
|
||||||
draw.text(home_logo_pos, game_details["home_abbr"], font=team_font, fill="white")
|
draw.text(home_logo_pos, game_details["home_abbr"], font=team_font, fill="white")
|
||||||
@@ -579,14 +580,13 @@ class NHLScoreboardManager:
|
|||||||
self.local_timezone = ZoneInfo(DEFAULT_TIMEZONE)
|
self.local_timezone = ZoneInfo(DEFAULT_TIMEZONE)
|
||||||
|
|
||||||
# State variables
|
# State variables
|
||||||
self.last_data_fetch_time = 0 # When API was last called
|
self.last_data_fetch_time = 0
|
||||||
self.last_logic_update_time = 0 # When selection logic was last run
|
self.last_logic_update_time = 0
|
||||||
self.relevant_events = [] # Events matching current display criteria
|
self.relevant_events: List[Dict[str, Any]] = [] # ALL relevant events (live, upcoming today, recent final)
|
||||||
self.current_event_index = 0 # Index for cycling through relevant_events
|
self.current_event_index: int = 0
|
||||||
self.last_cycle_time = 0 # Timestamp for cycling live games
|
self.last_cycle_time: float = 0
|
||||||
self.display_mode = 'none' # 'live', 'upcoming', 'recent_final', 'none', 'error'
|
self.current_display_details: Optional[Dict[str, Any]] = None
|
||||||
self.current_display_details = None # Details for the currently shown game in cycle
|
self.needs_redraw: bool = True
|
||||||
self.needs_redraw = True # Flag to force redraw in display()
|
|
||||||
|
|
||||||
# Get display dimensions
|
# Get display dimensions
|
||||||
if hasattr(display_manager, 'width') and hasattr(display_manager, 'height'):
|
if hasattr(display_manager, 'width') and hasattr(display_manager, 'height'):
|
||||||
@@ -709,63 +709,9 @@ class NHLScoreboardManager:
|
|||||||
logging.error(f"[NHL] Failed to load test data: {load_e}")
|
logging.error(f"[NHL] Failed to load test data: {load_e}")
|
||||||
return None # Return None if fetch fails
|
return None # Return None if fetch fails
|
||||||
|
|
||||||
def _extract_game_details(self, game_event):
|
def _find_events_by_criteria(self, all_events: List[Dict[str, Any]],
|
||||||
"""Extracts relevant details for the score bug display from raw event data."""
|
is_live: bool = False, is_upcoming_today: bool = False,
|
||||||
if not game_event:
|
is_recent_final: bool = False) -> List[Dict[str, Any]]:
|
||||||
return None
|
|
||||||
|
|
||||||
details = {}
|
|
||||||
try:
|
|
||||||
competition = game_event["competitions"][0]
|
|
||||||
status = competition["status"]
|
|
||||||
competitors = competition["competitors"]
|
|
||||||
game_date_str = game_event["date"]
|
|
||||||
|
|
||||||
try:
|
|
||||||
details["start_time_utc"] = datetime.fromisoformat(game_date_str.replace("Z", "+00:00"))
|
|
||||||
except (ValueError, KeyError):
|
|
||||||
logging.warning(f"[NHL] Could not parse game date: {game_date_str}")
|
|
||||||
details["start_time_utc"] = None
|
|
||||||
|
|
||||||
home_team = next((c for c in competitors if c.get("homeAway") == "home"), None)
|
|
||||||
away_team = next((c for c in competitors if c.get("homeAway") == "away"), None)
|
|
||||||
|
|
||||||
if not home_team or not away_team:
|
|
||||||
logging.warning(f"[NHL] Missing home or away team data in event: {game_event.get('id')}")
|
|
||||||
return None # Cannot proceed without both teams
|
|
||||||
|
|
||||||
details["status_text"] = status.get("type", {}).get("shortDetail", "N/A")
|
|
||||||
details["status_type_name"] = status.get("type", {}).get("name")
|
|
||||||
details["period"] = status.get("period", 0)
|
|
||||||
details["clock"] = status.get("displayClock", "0:00")
|
|
||||||
state = status.get("type", {}).get("state")
|
|
||||||
details["is_live"] = state in ("in", "halftime")
|
|
||||||
details["is_final"] = state == "post"
|
|
||||||
details["is_upcoming"] = state == "pre"
|
|
||||||
|
|
||||||
details["home_abbr"] = home_team.get("team", {}).get("abbreviation", "???")
|
|
||||||
details["home_score"] = home_team.get("score", "0")
|
|
||||||
details["home_logo_path"] = self.logo_dir / f"{details['home_abbr']}.png" if details['home_abbr'] != "???" else None
|
|
||||||
|
|
||||||
details["away_abbr"] = away_team.get("team", {}).get("abbreviation", "???")
|
|
||||||
details["away_score"] = away_team.get("score", "0")
|
|
||||||
details["away_logo_path"] = self.logo_dir / f"{details['away_abbr']}.png" if details['away_abbr'] != "???" else None
|
|
||||||
|
|
||||||
# Check logo files
|
|
||||||
if details["home_logo_path"] and not details["home_logo_path"].is_file():
|
|
||||||
logging.debug(f"[NHL] Home logo not found: {details['home_logo_path']}")
|
|
||||||
details["home_logo_path"] = None
|
|
||||||
if details["away_logo_path"] and not details["away_logo_path"].is_file():
|
|
||||||
logging.debug(f"[NHL] Away logo not found: {details['away_logo_path']}")
|
|
||||||
details["away_logo_path"] = None
|
|
||||||
|
|
||||||
return details
|
|
||||||
|
|
||||||
except (KeyError, IndexError, StopIteration, TypeError) as e:
|
|
||||||
logging.error(f"[NHL] Error parsing game details: {e} - Data snippet: {str(game_event)[:200]}...")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _find_events_by_criteria(self, all_events, is_live=False, is_upcoming_today=False, is_recent_final=False):
|
|
||||||
"""Helper to find favorite team events matching specific criteria."""
|
"""Helper to find favorite team events matching specific criteria."""
|
||||||
matches = []
|
matches = []
|
||||||
now_utc = datetime.now(timezone.utc)
|
now_utc = datetime.now(timezone.utc)
|
||||||
@@ -773,25 +719,25 @@ class NHLScoreboardManager:
|
|||||||
cutoff_time_utc = now_utc - timedelta(hours=RECENT_GAME_HOURS)
|
cutoff_time_utc = now_utc - timedelta(hours=RECENT_GAME_HOURS)
|
||||||
|
|
||||||
for event in all_events:
|
for event in all_events:
|
||||||
# Check if it's a favorite team's game first
|
|
||||||
competitors = event.get("competitions", [{}])[0].get("competitors", [])
|
competitors = event.get("competitions", [{}])[0].get("competitors", [])
|
||||||
if len(competitors) == 2:
|
if len(competitors) == 2:
|
||||||
team1_abbr = competitors[0].get("team", {}).get("abbreviation")
|
team1_abbr = competitors[0].get("team", {}).get("abbreviation")
|
||||||
team2_abbr = competitors[1].get("team", {}).get("abbreviation")
|
team2_abbr = competitors[1].get("team", {}).get("abbreviation")
|
||||||
is_favorite = team1_abbr in self.favorite_teams or team2_abbr in self.favorite_teams
|
is_favorite = team1_abbr in self.favorite_teams or team2_abbr in self.favorite_teams
|
||||||
|
# Skip non-favorites ONLY if show_only_favorites is true
|
||||||
if not is_favorite and self.show_only_favorites:
|
if not is_favorite and self.show_only_favorites:
|
||||||
continue # Skip if not favorite and only showing favorites
|
continue
|
||||||
|
|
||||||
# Now check criteria based on flags
|
# Apply criteria ONLY if the event involves a favorite team
|
||||||
|
if is_favorite:
|
||||||
details = self._extract_game_details(event)
|
details = self._extract_game_details(event)
|
||||||
if not details: continue # Skip if parsing failed
|
if not details: continue
|
||||||
|
|
||||||
if is_live and details["is_live"]:
|
if is_live and details["is_live"]:
|
||||||
matches.append(event)
|
matches.append(event)
|
||||||
continue # Found live, no need for further checks for this event
|
continue
|
||||||
|
|
||||||
if is_upcoming_today and details["is_upcoming"] and details["start_time_utc"]:
|
if is_upcoming_today and details["is_upcoming"] and details["start_time_utc"]:
|
||||||
# Check if start time is today in local timezone
|
|
||||||
start_local_date = details["start_time_utc"].astimezone(self.local_timezone).date()
|
start_local_date = details["start_time_utc"].astimezone(self.local_timezone).date()
|
||||||
if start_local_date == today_local and details["start_time_utc"] > now_utc:
|
if start_local_date == today_local and details["start_time_utc"] > now_utc:
|
||||||
matches.append(event)
|
matches.append(event)
|
||||||
@@ -801,174 +747,183 @@ class NHLScoreboardManager:
|
|||||||
if details["start_time_utc"] > cutoff_time_utc:
|
if details["start_time_utc"] > cutoff_time_utc:
|
||||||
matches.append(event)
|
matches.append(event)
|
||||||
continue
|
continue
|
||||||
# Add fallback logic if not showing only favorites? Or handle in update()?
|
# --- NOTE: No fallback logic here yet for non-favorites if show_only_favorites is false ---
|
||||||
# For now, this focuses on finding FAVORITE events based on criteria.
|
|
||||||
|
|
||||||
# Sort results appropriately
|
# Sort results appropriately
|
||||||
if is_live:
|
if is_live:
|
||||||
# Maybe sort live games by start time or some other metric?
|
pass # Keep API order for now
|
||||||
pass # Keep order for now
|
|
||||||
elif is_upcoming_today:
|
elif is_upcoming_today:
|
||||||
matches.sort(key=lambda x: self._extract_game_details(x).get("start_time_utc") or datetime.max.replace(tzinfo=timezone.utc)) # Sort by soonest start time
|
matches.sort(key=lambda x: self._extract_game_details(x).get("start_time_utc") or datetime.max.replace(tzinfo=timezone.utc)) # Sort by soonest
|
||||||
elif is_recent_final:
|
elif is_recent_final:
|
||||||
matches.sort(key=lambda x: self._extract_game_details(x).get("start_time_utc") or datetime.min.replace(tzinfo=timezone.utc), reverse=True) # Sort by most recent start time
|
matches.sort(key=lambda x: self._extract_game_details(x).get("start_time_utc") or datetime.min.replace(tzinfo=timezone.utc), reverse=True) # Sort by most recent
|
||||||
|
|
||||||
return matches
|
return matches
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Determines the correct state and events to display."""
|
"""Determines the list of events to cycle through based on priority."""
|
||||||
if not self.is_enabled:
|
if not self.is_enabled:
|
||||||
if self.display_mode != 'none':
|
if self.relevant_events: # Clear if disabled
|
||||||
self.display_mode = 'none'
|
|
||||||
self.relevant_events = []
|
self.relevant_events = []
|
||||||
self.current_event_index = 0
|
self.current_event_index = 0
|
||||||
self.needs_redraw = True
|
self.needs_redraw = True
|
||||||
return
|
return
|
||||||
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
# Determine required data fetch interval
|
# Use active interval for logic checks if we *might* be showing something, else idle
|
||||||
# Always use idle interval check; fetch logic inside handles actual API call timing
|
check_interval = self.update_interval if self.relevant_events else self.idle_update_interval
|
||||||
check_interval = self.idle_update_interval
|
|
||||||
# If currently showing live game, force more frequent logic checks
|
|
||||||
# This ensures we notice quickly if the game ends
|
|
||||||
if self.display_mode == 'live':
|
|
||||||
check_interval = self.update_interval
|
|
||||||
|
|
||||||
if now - self.last_logic_update_time < check_interval:
|
if now - self.last_logic_update_time < check_interval:
|
||||||
# Ensure live game cycling still happens even if logic doesn't run
|
# Live game cycling still needs to happen within display() based on its own timer
|
||||||
if self.display_mode == 'live' and len(self.relevant_events) > 1:
|
return
|
||||||
if now - self.last_cycle_time > self.cycle_duration:
|
|
||||||
self.current_event_index = (self.current_event_index + 1) % len(self.relevant_events)
|
|
||||||
self.current_display_details = self._extract_game_details(self.relevant_events[self.current_event_index])
|
|
||||||
self.last_cycle_time = now
|
|
||||||
self.needs_redraw = True
|
|
||||||
return # Interval not elapsed for full logic update
|
|
||||||
|
|
||||||
logging.info(f"[NHL] Running update logic (Check Interval: {check_interval}s)")
|
logging.info(f"[NHL] Running update logic (Check Interval: {check_interval}s)")
|
||||||
self.last_logic_update_time = now
|
self.last_logic_update_time = now
|
||||||
|
|
||||||
# Decide which dates to fetch based on current time and lookback/lookahead needs
|
# Fetch data if interval passed
|
||||||
|
all_events: List[Dict] = []
|
||||||
|
if now - self.last_data_fetch_time > self.update_interval:
|
||||||
today_local = datetime.now(self.local_timezone)
|
today_local = datetime.now(self.local_timezone)
|
||||||
dates_to_fetch = {
|
dates_to_fetch = {
|
||||||
(today_local - timedelta(days=2)).strftime('%Y%m%d'), # Two days ago (for 48h lookback)
|
(today_local - timedelta(days=2)).strftime('%Y%m%d'),
|
||||||
(today_local - timedelta(days=1)).strftime('%Y%m%d'), # Yesterday
|
(today_local - timedelta(days=1)).strftime('%Y%m%d'),
|
||||||
today_local.strftime('%Y%m%d'), # Today
|
today_local.strftime('%Y%m%d'),
|
||||||
(today_local + timedelta(days=1)).strftime('%Y%m%d') # Tomorrow (for upcoming today)
|
(today_local + timedelta(days=1)).strftime('%Y%m%d')
|
||||||
}
|
}
|
||||||
# Only fetch if data is stale based on *active* interval (ensures fresh data for live checks)
|
|
||||||
if now - self.last_data_fetch_time > self.update_interval:
|
|
||||||
all_events = self._fetch_data_for_dates(sorted(list(dates_to_fetch)))
|
all_events = self._fetch_data_for_dates(sorted(list(dates_to_fetch)))
|
||||||
else:
|
|
||||||
# Use potentially stale data if logic check is frequent but data fetch isn't needed yet
|
|
||||||
# Or should we force fetch here? Let's assume stale data is ok for now if fetch interval not met.
|
|
||||||
# This part needs refinement - how to handle stale data vs frequent logic checks?
|
|
||||||
# For simplicity now, let's assume we fetch every logic check for testing.
|
|
||||||
all_events = self._fetch_data_for_dates(sorted(list(dates_to_fetch)))
|
|
||||||
|
|
||||||
|
|
||||||
if not all_events:
|
if not all_events:
|
||||||
logging.warning("[NHL] No events found after fetching. Setting mode to error/none.")
|
logging.warning("[NHL] No events found after fetching.")
|
||||||
if self.display_mode != 'error':
|
# Decide how to handle fetch failure - clear existing or keep stale?
|
||||||
self.display_mode = 'error' # Or 'none'?
|
# Let's clear for now if fetch fails entirely
|
||||||
|
if self.relevant_events: # If we previously had events
|
||||||
self.relevant_events = []
|
self.relevant_events = []
|
||||||
self.current_event_index = 0
|
self.current_event_index = 0
|
||||||
|
self.current_display_details = None
|
||||||
|
self.needs_redraw = True
|
||||||
|
return # Stop update if fetch failed
|
||||||
|
else:
|
||||||
|
# Data not stale enough for API call, but logic check proceeds.
|
||||||
|
# How do we get all_events? Need to cache it?
|
||||||
|
# --> Problem: Can't re-evaluate criteria without fetching.
|
||||||
|
# --> Solution: Always fetch data when logic runs, but use interval for logic run itself.
|
||||||
|
# --> Let's revert: Fetch data based on fetch interval, run logic based on logic interval.
|
||||||
|
# --> Requires caching the fetched `all_events`. Let's add a cache.
|
||||||
|
|
||||||
|
# --- Let's stick to the previous version's combined logic/fetch interval for now ---
|
||||||
|
# --- and focus on combining the event lists ---
|
||||||
|
|
||||||
|
today_local = datetime.now(self.local_timezone)
|
||||||
|
dates_to_fetch = {
|
||||||
|
(today_local - timedelta(days=2)).strftime('%Y%m%d'),
|
||||||
|
(today_local - timedelta(days=1)).strftime('%Y%m%d'),
|
||||||
|
today_local.strftime('%Y%m%d'),
|
||||||
|
(today_local + timedelta(days=1)).strftime('%Y%m%d')
|
||||||
|
}
|
||||||
|
all_events = self._fetch_data_for_dates(sorted(list(dates_to_fetch)))
|
||||||
|
if not all_events:
|
||||||
|
logging.warning("[NHL] No events found after fetching.")
|
||||||
|
if self.relevant_events:
|
||||||
|
self.relevant_events = []
|
||||||
|
self.current_event_index = 0
|
||||||
|
self.current_display_details = None
|
||||||
self.needs_redraw = True
|
self.needs_redraw = True
|
||||||
return
|
return
|
||||||
|
|
||||||
# --- Determine State and Relevant Events ---
|
|
||||||
new_mode = 'none'
|
|
||||||
new_relevant_events = []
|
|
||||||
|
|
||||||
# 1. Check for Live Favorite Games
|
# --- Determine Combined List of Relevant Events ---
|
||||||
live_favorite_games = self._find_events_by_criteria(all_events, is_live=True)
|
live_events = self._find_events_by_criteria(all_events, is_live=True)
|
||||||
if live_favorite_games:
|
upcoming_events = self._find_events_by_criteria(all_events, is_upcoming_today=True)
|
||||||
new_mode = 'live'
|
recent_final_events = self._find_events_by_criteria(all_events, is_recent_final=True)
|
||||||
new_relevant_events = live_favorite_games
|
|
||||||
logging.info(f"[NHL] Found {len(live_favorite_games)} live favorite game(s).")
|
|
||||||
|
|
||||||
# 2. Check for Upcoming Favorite Games Today (if no live ones)
|
new_relevant_events_combined = []
|
||||||
if new_mode == 'none':
|
added_ids = set()
|
||||||
upcoming_today_games = self._find_events_by_criteria(all_events, is_upcoming_today=True)
|
|
||||||
if upcoming_today_games:
|
|
||||||
new_mode = 'upcoming'
|
|
||||||
new_relevant_events = [upcoming_today_games[0]] # Show only the soonest one
|
|
||||||
logging.info(f"[NHL] No live games. Found upcoming favorite game today.")
|
|
||||||
|
|
||||||
# 3. Check for Recent Favorite Finals (if no live or upcoming today)
|
# Add in order of priority, avoiding duplicates
|
||||||
if new_mode == 'none':
|
for event in live_events + upcoming_events + recent_final_events:
|
||||||
recent_finals = self._find_events_by_criteria(all_events, is_recent_final=True)
|
event_id = event.get("id")
|
||||||
if recent_finals:
|
if event_id and event_id not in added_ids:
|
||||||
new_mode = 'recent_final'
|
new_relevant_events_combined.append(event)
|
||||||
new_relevant_events = [recent_finals[0]] # Show only the most recent one
|
added_ids.add(event_id)
|
||||||
logging.info(f"[NHL] No live or upcoming games. Found recent favorite final.")
|
|
||||||
|
|
||||||
# 4. Fallback (if not show_only_favorites) - Currently handled by find criteria, maybe refine?
|
# --- TODO: Implement Fallback Logic if show_only_favorites is False ---
|
||||||
# For now, if show_only_favorites is true, and none of above, mode remains 'none'.
|
if not new_relevant_events_combined and not self.show_only_favorites:
|
||||||
# If show_only_favorites is false, need to implement fallback search here if desired.
|
logging.debug("[NHL] No relevant favorite games, show_only_favorites=false. Fallback needed.")
|
||||||
if new_mode == 'none' and not self.show_only_favorites:
|
# Add logic here to find non-favorite games based on priority if desired
|
||||||
logging.debug("[NHL] No relevant favorite games, show_only_favorites=false, applying fallback.")
|
pass # No fallback implemented yet
|
||||||
# Basic fallback: Show first live non-fav, or first upcoming non-fav today, or most recent non-fav final?
|
|
||||||
# Keeping it simple for now: if no favs, mode is 'none'. Refine fallback later if needed.
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
# --- Compare and Update State ---
|
||||||
|
old_event_ids = {e.get("id") for e in self.relevant_events if e}
|
||||||
|
new_event_ids = {e.get("id") for e in new_relevant_events_combined if e}
|
||||||
|
|
||||||
# --- Update State if Changed ---
|
if old_event_ids != new_event_ids:
|
||||||
if new_mode != self.display_mode or new_relevant_events != self.relevant_events:
|
logging.info(f"[NHL] Relevant events changed. New count: {len(new_relevant_events_combined)}")
|
||||||
logging.info(f"[NHL] Mode change: {self.display_mode} -> {new_mode}")
|
self.relevant_events = new_relevant_events_combined
|
||||||
logging.debug(f"[NHL] Relevant Events Change: {len(self.relevant_events)} -> {len(new_relevant_events)}")
|
self.current_event_index = 0
|
||||||
self.display_mode = new_mode
|
|
||||||
self.relevant_events = new_relevant_events
|
|
||||||
self.current_event_index = 0 # Reset index on mode/list change
|
|
||||||
self.last_cycle_time = time.time() # Reset cycle timer
|
self.last_cycle_time = time.time() # Reset cycle timer
|
||||||
|
# Load details for the first item immediately
|
||||||
self.current_display_details = self._extract_game_details(self.relevant_events[0] if self.relevant_events else None)
|
self.current_display_details = self._extract_game_details(self.relevant_events[0] if self.relevant_events else None)
|
||||||
self.needs_redraw = True
|
self.needs_redraw = True
|
||||||
elif self.display_mode == 'live':
|
elif self.relevant_events: # List content is same, check if details of *current* item changed
|
||||||
# If mode is still live, check if *details* of current game changed
|
|
||||||
current_event_in_list = self.relevant_events[self.current_event_index]
|
current_event_in_list = self.relevant_events[self.current_event_index]
|
||||||
new_details_for_current = self._extract_game_details(current_event_in_list)
|
new_details_for_current = self._extract_game_details(current_event_in_list)
|
||||||
if new_details_for_current != self.current_display_details:
|
# Compare specifically relevant fields (score, clock, period, status)
|
||||||
logging.debug("[NHL] Live game details updated.")
|
if self._details_changed_significantly(self.current_display_details, new_details_for_current):
|
||||||
|
logging.debug(f"[NHL] Details updated for current event index {self.current_event_index}")
|
||||||
self.current_display_details = new_details_for_current
|
self.current_display_details = new_details_for_current
|
||||||
self.needs_redraw = True
|
self.needs_redraw = True
|
||||||
# else: No change in mode, events, or live details
|
else:
|
||||||
|
logging.debug("[NHL] No significant change in details for current event.")
|
||||||
|
# else: No relevant events before or after
|
||||||
|
|
||||||
|
|
||||||
|
def _details_changed_significantly(self, old_details, new_details) -> bool:
|
||||||
|
"""Compare specific fields to see if a redraw is needed."""
|
||||||
|
if old_details is None and new_details is None: return False
|
||||||
|
if old_details is None or new_details is None: return True # Changed from something to nothing or vice-versa
|
||||||
|
|
||||||
|
fields_to_check = ['home_score', 'away_score', 'clock', 'period', 'is_live', 'is_final', 'is_upcoming']
|
||||||
|
for field in fields_to_check:
|
||||||
|
if old_details.get(field) != new_details.get(field):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def display(self, force_clear: bool = False):
|
def display(self, force_clear: bool = False):
|
||||||
"""Generates and displays the current frame based on the determined state."""
|
"""Generates and displays the current frame, handling cycling."""
|
||||||
if not self.is_enabled:
|
if not self.is_enabled:
|
||||||
return
|
return
|
||||||
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
redraw_this_frame = force_clear or self.needs_redraw
|
redraw_this_frame = force_clear or self.needs_redraw
|
||||||
|
|
||||||
# --- Handle Live Game Cycling ---
|
# --- Handle Cycling ---
|
||||||
if self.display_mode == 'live' and len(self.relevant_events) > 1:
|
if len(self.relevant_events) > 1: # Cycle if more than one relevant event exists
|
||||||
if now - self.last_cycle_time > self.cycle_duration:
|
if now - self.last_cycle_time > self.cycle_duration:
|
||||||
self.current_event_index = (self.current_event_index + 1) % len(self.relevant_events)
|
self.current_event_index = (self.current_event_index + 1) % len(self.relevant_events)
|
||||||
|
# Get details for the *new* index
|
||||||
self.current_display_details = self._extract_game_details(self.relevant_events[self.current_event_index])
|
self.current_display_details = self._extract_game_details(self.relevant_events[self.current_event_index])
|
||||||
self.last_cycle_time = now
|
self.last_cycle_time = now
|
||||||
redraw_this_frame = True # Force redraw on cycle
|
redraw_this_frame = True # Force redraw on cycle
|
||||||
logging.debug(f"[NHL] Cycling live game to index {self.current_event_index}")
|
logging.debug(f"[NHL] Cycling to event index {self.current_event_index}")
|
||||||
elif self.current_display_details is None: # Ensure details are loaded initially
|
elif self.current_display_details is None: # Ensure details loaded for index 0 initially
|
||||||
self.current_display_details = self._extract_game_details(self.relevant_events[self.current_event_index])
|
self.current_display_details = self._extract_game_details(self.relevant_events[0])
|
||||||
redraw_this_frame = True
|
redraw_this_frame = True # Force redraw if details were missing
|
||||||
elif self.display_mode != 'live':
|
elif len(self.relevant_events) == 1:
|
||||||
# Ensure details are loaded if mode is not live (handles initial state or mode change)
|
# If only one event, make sure its details are loaded
|
||||||
if self.current_display_details is None and self.relevant_events:
|
if self.current_display_details is None:
|
||||||
self.current_display_details = self._extract_game_details(self.relevant_events[0])
|
self.current_display_details = self._extract_game_details(self.relevant_events[0])
|
||||||
redraw_this_frame = True
|
redraw_this_frame = True
|
||||||
elif not self.relevant_events: # No relevant events found
|
else: # No relevant events
|
||||||
self.current_display_details = None # Ensure details are cleared
|
if self.current_display_details is not None: # Clear details if list is empty now
|
||||||
|
self.current_display_details = None
|
||||||
|
redraw_this_frame = True
|
||||||
|
|
||||||
|
|
||||||
# --- Generate and Display Frame ---
|
# --- Generate and Display Frame ---
|
||||||
if not redraw_this_frame:
|
if not redraw_this_frame:
|
||||||
# logging.debug("[NHL] display() called but no redraw needed.")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
logging.debug(f"[NHL] Generating frame for mode: {self.display_mode} (Index: {self.current_event_index})")
|
logging.debug(f"[NHL] Generating frame (Index: {self.current_event_index})")
|
||||||
# Use self.current_display_details which is updated by cycle logic or initial load
|
frame = self._create_frame(self.current_display_details) # Pass the specific details
|
||||||
frame = self._create_frame(self.current_display_details)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if hasattr(self.display_manager, 'display_image'):
|
if hasattr(self.display_manager, 'display_image'):
|
||||||
@@ -978,48 +933,44 @@ class NHLScoreboardManager:
|
|||||||
else:
|
else:
|
||||||
logging.error("[NHL] DisplayManager missing display_image or matrix.SetImage method.")
|
logging.error("[NHL] DisplayManager missing display_image or matrix.SetImage method.")
|
||||||
|
|
||||||
self.needs_redraw = False # Reset flag after successful display attempt
|
self.needs_redraw = False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"[NHL] Error displaying frame via DisplayManager: {e}")
|
logging.error(f"[NHL] Error displaying frame via DisplayManager: {e}")
|
||||||
|
|
||||||
|
|
||||||
def _create_frame(self, game_details):
|
def _create_frame(self, game_details: Optional[Dict[str, Any]]) -> Image.Image:
|
||||||
"""Creates a Pillow image frame based on game details and current display mode."""
|
"""Creates a Pillow image frame based on game details."""
|
||||||
# Determine layout based on self.display_mode and game_details
|
# This method now simply renders the layout based on the details passed in
|
||||||
# Note: game_details will be None if mode is 'none' or 'error'
|
|
||||||
|
|
||||||
img = Image.new('RGB', (self.display_width, self.display_height), color='black')
|
img = Image.new('RGB', (self.display_width, self.display_height), color='black')
|
||||||
draw = ImageDraw.Draw(img)
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
# --- Choose Layout ---
|
if not game_details:
|
||||||
if self.display_mode == 'live' and game_details:
|
|
||||||
self._draw_scorebug_layout(draw, game_details)
|
|
||||||
elif self.display_mode == 'upcoming' and game_details:
|
|
||||||
self._draw_upcoming_layout(draw, game_details)
|
|
||||||
elif self.display_mode == 'recent_final' and game_details:
|
|
||||||
self._draw_scorebug_layout(draw, game_details) # Use scorebug for final
|
|
||||||
else: # 'none' or 'error'
|
|
||||||
self._draw_placeholder_layout(draw)
|
self._draw_placeholder_layout(draw)
|
||||||
|
elif game_details.get("is_upcoming"):
|
||||||
|
self._draw_upcoming_layout(draw, game_details)
|
||||||
|
elif game_details.get("is_live") or game_details.get("is_final"):
|
||||||
|
self._draw_scorebug_layout(draw, game_details)
|
||||||
|
else: # Fallback/Other states
|
||||||
|
self._draw_placeholder_layout(draw, msg=game_details.get("status_text", "NHL Status")) # Show status text
|
||||||
|
|
||||||
return img
|
return img
|
||||||
|
|
||||||
def _draw_placeholder_layout(self, draw):
|
def _draw_placeholder_layout(self, draw: ImageDraw.ImageDraw, msg: str = "No NHL Games"):
|
||||||
"""Draws the 'No NHL Games' message."""
|
"""Draws the 'No NHL Games' or other placeholder message."""
|
||||||
font = self.fonts.get('placeholder', ImageFont.load_default())
|
font = self.fonts.get('placeholder', ImageFont.load_default())
|
||||||
msg = "No NHL Games"
|
|
||||||
bbox = draw.textbbox((0,0), msg, font=font)
|
bbox = draw.textbbox((0,0), msg, font=font)
|
||||||
text_width = bbox[2] - bbox[0]
|
text_width = bbox[2] - bbox[0]
|
||||||
text_height = bbox[3] - bbox[1]
|
text_height = bbox[3] - bbox[1]
|
||||||
draw.text(((self.display_width - text_width) // 2, (self.display_height - text_height) // 2),
|
draw.text(((self.display_width - text_width) // 2, (self.display_height - text_height) // 2),
|
||||||
msg, font=font, fill='grey')
|
msg, font=font, fill='grey')
|
||||||
|
|
||||||
def _draw_upcoming_layout(self, draw, game_details):
|
def _draw_upcoming_layout(self, draw: ImageDraw.ImageDraw, game_details: Dict[str, Any]):
|
||||||
"""Draws the layout for an upcoming game."""
|
"""Draws the layout for an upcoming game."""
|
||||||
font_team = self.fonts.get('team', ImageFont.load_default())
|
font_team = self.fonts.get('team', ImageFont.load_default())
|
||||||
font_main = self.fonts.get('upcoming_main', ImageFont.load_default())
|
font_main = self.fonts.get('upcoming_main', ImageFont.load_default())
|
||||||
font_vs = self.fonts.get('upcoming_vs', ImageFont.load_default())
|
font_vs = self.fonts.get('upcoming_vs', ImageFont.load_default())
|
||||||
img = draw.im # Get the image object associated with the draw object
|
img = draw.im
|
||||||
|
|
||||||
logging.debug("[NHL] Drawing upcoming game layout.")
|
logging.debug("[NHL] Drawing upcoming game layout.")
|
||||||
|
|
||||||
@@ -1037,7 +988,7 @@ class NHLScoreboardManager:
|
|||||||
try:
|
try:
|
||||||
away_logo = Image.open(game_details["away_logo_path"]).convert("RGBA")
|
away_logo = Image.open(game_details["away_logo_path"]).convert("RGBA")
|
||||||
away_logo.thumbnail(logo_size, Image.Resampling.LANCZOS)
|
away_logo.thumbnail(logo_size, Image.Resampling.LANCZOS)
|
||||||
img.paste(away_logo, (away_logo_x, (self.display_height - away_logo.height) // 2), away_logo)
|
img.paste(away_logo, (away_logo_x, (self.display_height - away_logo.height) // 2), mask=away_logo)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"[NHL] Error rendering upcoming away logo {game_details['away_logo_path']}: {e}")
|
logging.error(f"[NHL] Error rendering upcoming away logo {game_details['away_logo_path']}: {e}")
|
||||||
draw.text((away_logo_x, 5), game_details.get("away_abbr", "?"), font=font_team, fill="white")
|
draw.text((away_logo_x, 5), game_details.get("away_abbr", "?"), font=font_team, fill="white")
|
||||||
@@ -1049,7 +1000,7 @@ class NHLScoreboardManager:
|
|||||||
try:
|
try:
|
||||||
home_logo = Image.open(game_details["home_logo_path"]).convert("RGBA")
|
home_logo = Image.open(game_details["home_logo_path"]).convert("RGBA")
|
||||||
home_logo.thumbnail(logo_size, Image.Resampling.LANCZOS)
|
home_logo.thumbnail(logo_size, Image.Resampling.LANCZOS)
|
||||||
img.paste(home_logo, (home_logo_x, (self.display_height - home_logo.height) // 2), home_logo)
|
img.paste(home_logo, (home_logo_x, (self.display_height - home_logo.height) // 2), mask=home_logo)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"[NHL] Error rendering upcoming home logo {game_details['home_logo_path']}: {e}")
|
logging.error(f"[NHL] Error rendering upcoming home logo {game_details['home_logo_path']}: {e}")
|
||||||
draw.text((home_logo_x, 5), game_details.get("home_abbr", "?"), font=font_team, fill="white")
|
draw.text((home_logo_x, 5), game_details.get("home_abbr", "?"), font=font_team, fill="white")
|
||||||
@@ -1092,7 +1043,7 @@ class NHLScoreboardManager:
|
|||||||
draw.text((center_x, vs_y), vs_str, font=font_vs, fill='white', anchor="mt")
|
draw.text((center_x, vs_y), vs_str, font=font_vs, fill='white', anchor="mt")
|
||||||
|
|
||||||
|
|
||||||
def _draw_scorebug_layout(self, draw, game_details):
|
def _draw_scorebug_layout(self, draw: ImageDraw.ImageDraw, game_details: Dict[str, Any]):
|
||||||
"""Draws the standard score bug layout for live or final games."""
|
"""Draws the standard score bug layout for live or final games."""
|
||||||
font_score = self.fonts.get('score', ImageFont.load_default())
|
font_score = self.fonts.get('score', ImageFont.load_default())
|
||||||
font_time = self.fonts.get('time', ImageFont.load_default())
|
font_time = self.fonts.get('time', ImageFont.load_default())
|
||||||
@@ -1121,7 +1072,7 @@ class NHLScoreboardManager:
|
|||||||
try:
|
try:
|
||||||
away_logo = Image.open(game_details["away_logo_path"]).convert("RGBA")
|
away_logo = Image.open(game_details["away_logo_path"]).convert("RGBA")
|
||||||
away_logo.thumbnail(logo_size, Image.Resampling.LANCZOS)
|
away_logo.thumbnail(logo_size, Image.Resampling.LANCZOS)
|
||||||
img.paste(away_logo, (away_logo_x, (self.display_height - away_logo.height) // 2), away_logo)
|
img.paste(away_logo, (away_logo_x, (self.display_height - away_logo.height) // 2), mask=away_logo)
|
||||||
away_logo_drawn_size = away_logo.size
|
away_logo_drawn_size = away_logo.size
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"[NHL] Error rendering away logo {game_details['away_logo_path']}: {e}")
|
logging.error(f"[NHL] Error rendering away logo {game_details['away_logo_path']}: {e}")
|
||||||
@@ -1138,7 +1089,7 @@ class NHLScoreboardManager:
|
|||||||
try:
|
try:
|
||||||
home_logo = Image.open(game_details["home_logo_path"]).convert("RGBA")
|
home_logo = Image.open(game_details["home_logo_path"]).convert("RGBA")
|
||||||
home_logo.thumbnail(logo_size, Image.Resampling.LANCZOS)
|
home_logo.thumbnail(logo_size, Image.Resampling.LANCZOS)
|
||||||
img.paste(home_logo, (home_logo_x, (self.display_height - home_logo.height) // 2), home_logo)
|
img.paste(home_logo, (home_logo_x, (self.display_height - home_logo.height) // 2), mask=home_logo)
|
||||||
home_logo_drawn_size = home_logo.size
|
home_logo_drawn_size = home_logo.size
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"[NHL] Error rendering home logo {game_details['home_logo_path']}: {e}")
|
logging.error(f"[NHL] Error rendering home logo {game_details['home_logo_path']}: {e}")
|
||||||
|
|||||||
Reference in New Issue
Block a user