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

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}")