rework NHL to call displays individually

This commit is contained in:
ChuckBuilds
2025-04-18 10:33:19 -05:00
parent 77242c139b
commit 1966e9dd4b
2 changed files with 371 additions and 50 deletions

View File

@@ -7,7 +7,7 @@ from src.display_manager import DisplayManager
from src.config_manager import ConfigManager
from src.stock_manager import StockManager
from src.stock_news_manager import StockNewsManager
from src.nhl_scoreboard import NHLScoreboardManager
from src.nhl_managers import NHLLiveManager, NHLRecentManager, NHLUpcomingManager
# Configure logging
logging.basicConfig(level=logging.INFO)
@@ -24,30 +24,44 @@ class DisplayController:
self.weather = WeatherManager(self.config, self.display_manager) if self.config.get('weather', {}).get('enabled', False) else None
self.stocks = StockManager(self.config, self.display_manager) if self.config.get('stocks', {}).get('enabled', False) else None
self.news = StockNewsManager(self.config, self.display_manager) if self.config.get('stock_news', {}).get('enabled', False) else None
self.nhl = NHLScoreboardManager(self.config, self.display_manager) if self.config.get('nhl_scoreboard', {}).get('enabled', False) else None
# Initialize NHL managers if NHL is enabled
nhl_enabled = self.config.get('nhl_scoreboard', {}).get('enabled', False)
if nhl_enabled:
self.nhl_live = NHLLiveManager(self.config, self.display_manager)
self.nhl_recent = NHLRecentManager(self.config, self.display_manager)
self.nhl_upcoming = NHLUpcomingManager(self.config, self.display_manager)
else:
self.nhl_live = None
self.nhl_recent = None
self.nhl_upcoming = None
# List of available display modes (adjust order as desired)
self.available_modes = []
if self.clock: self.available_modes.append('clock')
if self.weather: self.available_modes.extend(['weather_current', 'weather_hourly', 'weather_daily']) # Treat weather modes separately
if self.weather: self.available_modes.extend(['weather_current', 'weather_hourly', 'weather_daily'])
if self.stocks: self.available_modes.append('stocks')
if self.news: self.available_modes.append('stock_news') # News after Stocks
if self.nhl: self.available_modes.append('nhl') # NHL after News
if self.news: self.available_modes.append('stock_news')
if nhl_enabled:
self.available_modes.extend(['nhl_live', 'nhl_recent', 'nhl_upcoming'])
# Set initial display to first available mode
self.current_mode_index = 0
self.current_display_mode = self.available_modes[0] if self.available_modes else 'none' # Default if nothing enabled
self.current_display_mode = self.available_modes[0] if self.available_modes else 'none'
self.last_switch = time.time()
self.force_clear = True # Start with a clear screen
self.update_interval = 0.1 # Faster check loop
# Update display durations to include NHL
self.force_clear = True
self.update_interval = 0.1
# Update display durations to include NHL modes
self.display_durations = self.config['display'].get('display_durations', {
'clock': 15,
'weather_current': 15,
'weather_hourly': 15,
'weather_daily': 15,
'stocks': 45,
'nhl': 30, # Default NHL duration
'nhl_live': 30, # Live games update more frequently
'nhl_recent': 20, # Recent games
'nhl_upcoming': 20, # Upcoming games
'stock_news': 30
})
logger.info("DisplayController initialized with display_manager: %s", id(self.display_manager))
@@ -55,59 +69,53 @@ class DisplayController:
def get_current_duration(self) -> int:
"""Get the duration for the current display mode."""
# Use the unified current_display_mode
mode_key = self.current_display_mode
# Map weather sub-modes if needed for duration lookup
if mode_key.startswith('weather_'):
duration_key = mode_key.split('_', 1)[1] # current, hourly, daily
if duration_key == 'current': duration_key = 'weather' # Match config key
elif duration_key == 'hourly': duration_key = 'hourly_forecast'
elif duration_key == 'daily': duration_key = 'daily_forecast'
else: duration_key = 'weather' # Fallback
return self.display_durations.get(duration_key, 15)
duration_key = mode_key.split('_', 1)[1]
if duration_key == 'current': duration_key = 'weather'
elif duration_key == 'hourly': duration_key = 'hourly_forecast'
elif duration_key == 'daily': duration_key = 'daily_forecast'
else: duration_key = 'weather'
return self.display_durations.get(duration_key, 15)
return self.display_durations.get(mode_key, 15) # Default duration 15s
return self.display_durations.get(mode_key, 15)
def _update_modules(self):
"""Call update methods on active managers."""
# Update methods might have different frequencies, but call here for simplicity
# Could add timers per module later if needed
if self.weather: self.weather.get_weather() # weather update fetches data
if self.stocks: self.stocks.update_stock_data() # Correct method name
if self.news: self.news.update_news_data() # Correct method name
if self.nhl: self.nhl.update()
# Clock updates itself during display typically
if self.weather: self.weather.get_weather()
if self.stocks: self.stocks.update_stock_data()
if self.news: self.news.update_news_data()
# Update NHL managers
if self.nhl_live: self.nhl_live.update()
if self.nhl_recent: self.nhl_recent.update()
if self.nhl_upcoming: self.nhl_upcoming.update()
def run(self):
"""Run the display controller, switching between displays."""
if not self.available_modes:
logger.warning("No display modes are enabled. Exiting.")
self.display_manager.cleanup()
return
logger.warning("No display modes are enabled. Exiting.")
self.display_manager.cleanup()
return
try:
while True:
current_time = time.time()
# --- Update Data for Modules ---
# Call update method for all relevant modules periodically
# (Frequency can be optimized later if needed)
self._update_modules()
# Update data for all modules
self._update_modules()
# --- Check for Mode Switch ---
# Check for mode switch
if current_time - self.last_switch > self.get_current_duration():
self.current_mode_index = (self.current_mode_index + 1) % len(self.available_modes)
self.current_display_mode = self.available_modes[self.current_mode_index]
logger.info(f"Switching display to: {self.current_display_mode}")
self.last_switch = current_time
self.force_clear = True # Force clear on mode switch
# Clearing is likely handled by the display method or display_manager now
# self.display_manager.clear()
self.force_clear = True
# --- Display Current Mode Frame ---
# Display current mode frame
try:
# Simplified display logic based on mode string
if self.current_display_mode == 'clock' and self.clock:
self.clock.display_time(force_clear=self.force_clear)
@@ -121,23 +129,22 @@ class DisplayController:
elif self.current_display_mode == 'stocks' and self.stocks:
self.stocks.display_stocks(force_clear=self.force_clear)
elif self.current_display_mode == 'nhl' and self.nhl:
self.nhl.display(force_clear=self.force_clear)
elif self.current_display_mode == 'nhl_live' and self.nhl_live:
self.nhl_live.display(force_clear=self.force_clear)
elif self.current_display_mode == 'nhl_recent' and self.nhl_recent:
self.nhl_recent.display(force_clear=self.force_clear)
elif self.current_display_mode == 'nhl_upcoming' and self.nhl_upcoming:
self.nhl_upcoming.display(force_clear=self.force_clear)
elif self.current_display_mode == 'stock_news' and self.news:
self.news.display_news() # Removed force_clear argument
self.news.display_news()
except Exception as e:
logger.error(f"Error updating display for mode {self.current_display_mode}: {e}", exc_info=True)
# Avoid busy-looping on error, maybe skip frame or wait?
time.sleep(1)
continue # Skip rest of loop iteration
time.sleep(1)
continue
# Reset force clear flag after the first successful display in a mode
self.force_clear = False
# Main loop delay - REMOVED for faster processing/scrolling
# time.sleep(self.update_interval)
self.force_clear = False
except KeyboardInterrupt:
print("\nDisplay stopped by user")

314
src/nhl_managers.py Normal file
View File

@@ -0,0 +1,314 @@
import os
import time
import logging
from typing import Dict, Any, Optional, List
from PIL import Image, ImageDraw, ImageFont
from pathlib import Path
from datetime import datetime, timedelta, timezone
class BaseNHLManager:
"""Base class for NHL managers with common functionality."""
def __init__(self, config: dict, display_manager):
self.display_manager = display_manager
self.config = config
self.nhl_config = config.get("nhl_scoreboard", {})
self.is_enabled = self.nhl_config.get("enabled", False)
self.logo_dir = Path(config.get("nhl_scoreboard", {}).get("logo_dir", "assets/sports/nhl_logos"))
self.update_interval = self.nhl_config.get("update_interval_seconds", 60)
self.last_update = 0
self.current_game = None
self.fonts = self._load_fonts()
def _load_fonts(self):
"""Load fonts used by the scoreboard."""
fonts = {}
try:
fonts['score'] = ImageFont.truetype("arial.ttf", 12)
fonts['time'] = ImageFont.truetype("arial.ttf", 10)
fonts['team'] = ImageFont.truetype("arial.ttf", 8)
fonts['status'] = ImageFont.truetype("arial.ttf", 9)
except IOError:
logging.warning("[NHL] Arial font not found, using default PIL font.")
fonts['score'] = ImageFont.load_default()
fonts['time'] = ImageFont.load_default()
fonts['team'] = ImageFont.load_default()
fonts['status'] = ImageFont.load_default()
return fonts
def _extract_game_details(self, game_event):
"""Extract game details from an event."""
if not game_event:
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:
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")
away_team = next(c for c in competitors if c.get("homeAway") == "away")
details["status_text"] = status["type"]["shortDetail"]
details["period"] = status.get("period", 0)
details["clock"] = status.get("displayClock", "0:00")
details["is_live"] = status["type"]["state"] in ("in", "halftime")
details["is_final"] = status["type"]["state"] == "post"
details["is_upcoming"] = status["type"]["state"] == "pre"
details["home_abbr"] = home_team["team"]["abbreviation"]
details["home_score"] = home_team.get("score", "0")
details["home_logo_path"] = self.logo_dir / f"{details['home_abbr']}.png"
details["away_abbr"] = away_team["team"]["abbreviation"]
details["away_score"] = away_team.get("score", "0")
details["away_logo_path"] = self.logo_dir / f"{details['away_abbr']}.png"
# Validate logo files
for logo_type in ['home', 'away']:
logo_path = details[f"{logo_type}_logo_path"]
if not logo_path.is_file():
logging.warning(f"[NHL] {logo_type.title()} logo not found: {logo_path}")
details[f"{logo_type}_logo_path"] = None
else:
try:
with Image.open(logo_path) as img:
logging.debug(f"[NHL] {logo_type.title()} logo is valid: {img.format}, size: {img.size}")
except Exception as e:
logging.error(f"[NHL] {logo_type.title()} logo file exists but is not valid: {e}")
details[f"{logo_type}_logo_path"] = None
return details
except Exception as e:
logging.error(f"[NHL] Error parsing game details: {e}")
return None
def _load_and_resize_logo(self, logo_path: Path, max_size: tuple) -> Optional[Image.Image]:
"""Load and resize a logo image."""
if not logo_path or not logo_path.is_file():
return None
try:
logo = Image.open(logo_path)
if logo.mode != 'RGBA':
logo = logo.convert('RGBA')
logo.thumbnail(max_size, Image.Resampling.LANCZOS)
return logo
except Exception as e:
logging.error(f"[NHL] Error loading logo {logo_path}: {e}")
return None
class NHLLiveManager(BaseNHLManager):
"""Manager for live NHL games."""
def __init__(self, config: dict, display_manager):
super().__init__(config, display_manager)
self.update_interval = self.nhl_config.get("live_update_interval", 30) # More frequent updates for live games
def update(self):
"""Update live game data."""
current_time = time.time()
if current_time - self.last_update < self.update_interval:
return
# TODO: Implement live game data fetching
self.last_update = current_time
def display(self, force_clear: bool = False):
"""Display live game information."""
if not self.current_game:
return
try:
img = Image.new('RGB', (self.display_manager.width, self.display_manager.height), 'black')
draw = ImageDraw.Draw(img)
# Load and resize logos
max_size = (self.display_manager.width // 3, self.display_manager.height // 2)
home_logo = self._load_and_resize_logo(self.current_game["home_logo_path"], max_size)
away_logo = self._load_and_resize_logo(self.current_game["away_logo_path"], max_size)
# Draw logos
if home_logo:
home_x = self.display_manager.width // 4 - home_logo.width // 2
home_y = self.display_manager.height // 4 - home_logo.height // 2
temp_img = Image.new('RGB', (self.display_manager.width, self.display_manager.height), 'black')
temp_draw = ImageDraw.Draw(temp_img)
temp_draw.im.paste(home_logo, (home_x, home_y), home_logo)
draw.im.paste(temp_img, (0, 0))
if away_logo:
away_x = self.display_manager.width // 4 - away_logo.width // 2
away_y = 3 * self.display_manager.height // 4 - away_logo.height // 2
temp_img = Image.new('RGB', (self.display_manager.width, self.display_manager.height), 'black')
temp_draw = ImageDraw.Draw(temp_img)
temp_draw.im.paste(away_logo, (away_x, away_y), away_logo)
draw.im.paste(temp_img, (0, 0))
# Draw scores
home_score = str(self.current_game["home_score"])
away_score = str(self.current_game["away_score"])
home_score_x = self.display_manager.width // 2 - 10
home_score_y = self.display_manager.height // 4 - 8
away_score_x = self.display_manager.width // 2 - 10
away_score_y = 3 * self.display_manager.height // 4 - 8
draw.text((home_score_x, home_score_y), home_score, font=self.fonts['score'], fill=(255, 255, 255))
draw.text((away_score_x, away_score_y), away_score, font=self.fonts['score'], fill=(255, 255, 255))
# Draw game status (period and time)
period = self.current_game["period"]
clock = self.current_game["clock"]
period_str = f"{period}{'st' if period==1 else 'nd' if period==2 else 'rd' if period==3 else 'th'}"
status_x = self.display_manager.width // 2 - 20
status_y = self.display_manager.height // 2 - 8
draw.text((status_x, status_y), f"{period_str} {clock}", font=self.fonts['status'], fill=(255, 255, 255))
self.display_manager.display_image(img)
except Exception as e:
logging.error(f"[NHL] Error displaying live game: {e}")
class NHLRecentManager(BaseNHLManager):
"""Manager for recently completed NHL games."""
def __init__(self, config: dict, display_manager):
super().__init__(config, display_manager)
self.recent_hours = self.nhl_config.get("recent_game_hours", 48)
self.update_interval = self.nhl_config.get("recent_update_interval", 300) # 5 minutes
def update(self):
"""Update recent game data."""
current_time = time.time()
if current_time - self.last_update < self.update_interval:
return
# TODO: Implement recent game data fetching
self.last_update = current_time
def display(self, force_clear: bool = False):
"""Display recent game information."""
if not self.current_game:
return
try:
img = Image.new('RGB', (self.display_manager.width, self.display_manager.height), 'black')
draw = ImageDraw.Draw(img)
# Load and resize logos
max_size = (self.display_manager.width // 3, self.display_manager.height // 2)
home_logo = self._load_and_resize_logo(self.current_game["home_logo_path"], max_size)
away_logo = self._load_and_resize_logo(self.current_game["away_logo_path"], max_size)
# Draw logos
if home_logo:
home_x = self.display_manager.width // 4 - home_logo.width // 2
home_y = self.display_manager.height // 4 - home_logo.height // 2
temp_img = Image.new('RGB', (self.display_manager.width, self.display_manager.height), 'black')
temp_draw = ImageDraw.Draw(temp_img)
temp_draw.im.paste(home_logo, (home_x, home_y), home_logo)
draw.im.paste(temp_img, (0, 0))
if away_logo:
away_x = self.display_manager.width // 4 - away_logo.width // 2
away_y = 3 * self.display_manager.height // 4 - away_logo.height // 2
temp_img = Image.new('RGB', (self.display_manager.width, self.display_manager.height), 'black')
temp_draw = ImageDraw.Draw(temp_img)
temp_draw.im.paste(away_logo, (away_x, away_y), away_logo)
draw.im.paste(temp_img, (0, 0))
# Draw scores
home_score = str(self.current_game["home_score"])
away_score = str(self.current_game["away_score"])
home_score_x = self.display_manager.width // 2 - 10
home_score_y = self.display_manager.height // 4 - 8
away_score_x = self.display_manager.width // 2 - 10
away_score_y = 3 * self.display_manager.height // 4 - 8
draw.text((home_score_x, home_score_y), home_score, font=self.fonts['score'], fill=(255, 255, 255))
draw.text((away_score_x, away_score_y), away_score, font=self.fonts['score'], fill=(255, 255, 255))
# Draw "FINAL" status
status_x = self.display_manager.width // 2 - 20
status_y = self.display_manager.height // 2 - 8
draw.text((status_x, status_y), "FINAL", font=self.fonts['status'], fill=(255, 0, 0))
self.display_manager.display_image(img)
except Exception as e:
logging.error(f"[NHL] Error displaying recent game: {e}")
class NHLUpcomingManager(BaseNHLManager):
"""Manager for upcoming NHL games."""
def __init__(self, config: dict, display_manager):
super().__init__(config, display_manager)
self.update_interval = self.nhl_config.get("upcoming_update_interval", 300) # 5 minutes
def update(self):
"""Update upcoming game data."""
current_time = time.time()
if current_time - self.last_update < self.update_interval:
return
# TODO: Implement upcoming game data fetching
self.last_update = current_time
def display(self, force_clear: bool = False):
"""Display upcoming game information."""
if not self.current_game:
return
try:
img = Image.new('RGB', (self.display_manager.width, self.display_manager.height), 'black')
draw = ImageDraw.Draw(img)
# Load and resize logos
max_size = (self.display_manager.width // 3, self.display_manager.height // 2)
home_logo = self._load_and_resize_logo(self.current_game["home_logo_path"], max_size)
away_logo = self._load_and_resize_logo(self.current_game["away_logo_path"], max_size)
# Draw logos
if home_logo:
home_x = self.display_manager.width // 4 - home_logo.width // 2
home_y = self.display_manager.height // 4 - home_logo.height // 2
temp_img = Image.new('RGB', (self.display_manager.width, self.display_manager.height), 'black')
temp_draw = ImageDraw.Draw(temp_img)
temp_draw.im.paste(home_logo, (home_x, home_y), home_logo)
draw.im.paste(temp_img, (0, 0))
if away_logo:
away_x = self.display_manager.width // 4 - away_logo.width // 2
away_y = 3 * self.display_manager.height // 4 - away_logo.height // 2
temp_img = Image.new('RGB', (self.display_manager.width, self.display_manager.height), 'black')
temp_draw = ImageDraw.Draw(temp_img)
temp_draw.im.paste(away_logo, (away_x, away_y), away_logo)
draw.im.paste(temp_img, (0, 0))
# Draw game time
start_time = self.current_game["start_time_utc"]
if start_time:
local_time = start_time.astimezone()
time_str = local_time.strftime("%I:%M %p").lstrip('0')
date_str = local_time.strftime("%a %b %d")
time_x = self.display_manager.width // 2 - 20
time_y = self.display_manager.height // 2 - 8
draw.text((time_x, time_y), time_str, font=self.fonts['time'], fill=(0, 255, 255))
date_x = self.display_manager.width // 2 - 20
date_y = time_y + 10
draw.text((date_x, date_y), date_str, font=self.fonts['status'], fill=(0, 255, 255))
self.display_manager.display_image(img)
except Exception as e:
logging.error(f"[NHL] Error displaying upcoming game: {e}")