Add MLB manager with live game display, team rotation, and integration with display controller

This commit is contained in:
ChuckBuilds
2025-04-24 10:45:47 -05:00
parent 456a4d252f
commit a8a35a1ffc
3 changed files with 689 additions and 178 deletions

View File

@@ -39,7 +39,10 @@
"nba_recent": 20,
"nba_upcoming": 20,
"calendar": 30,
"youtube": 20
"youtube": 20,
"mlb_live": 30,
"mlb_recent": 20,
"mlb_upcoming": 20
}
},
"clock": {
@@ -122,5 +125,22 @@
"youtube": {
"enabled": true,
"update_interval": 3600
},
"mlb": {
"enabled": true,
"test_mode": true,
"update_interval_seconds": 300,
"live_update_interval": 20,
"recent_update_interval": 3600,
"upcoming_update_interval": 3600,
"recent_game_hours": 48,
"favorite_teams": ["TB", "TEX"],
"logo_dir": "assets/sports/mlb_logos",
"display_modes": {
"mlb_live": true,
"mlb_recent": true,
"mlb_upcoming": true
},
"live_game_duration": 30
}
}

View File

@@ -19,6 +19,7 @@ from src.stock_manager import StockManager
from src.stock_news_manager import StockNewsManager
from src.nhl_managers import NHLLiveManager, NHLRecentManager, NHLUpcomingManager
from src.nba_managers import NBALiveManager, NBARecentManager, NBAUpcomingManager
from src.mlb_manager import MBLLiveManager, MLBRecentManager, MLBUpcomingManager
from src.youtube_display import YouTubeDisplay
from src.calendar_manager import CalendarManager
@@ -75,6 +76,26 @@ class DisplayController:
self.nba_live = None
self.nba_recent = None
self.nba_upcoming = None
# Initialize MLB managers if enabled
mlb_time = time.time()
mlb_enabled = self.config.get('mlb', {}).get('enabled', False)
mlb_display_modes = self.config.get('mlb', {}).get('display_modes', {})
if mlb_enabled:
self.mlb_live = MBLLiveManager(self.config, self.display_manager) if mlb_display_modes.get('mlb_live', True) else None
self.mlb_recent = MLBRecentManager(self.config, self.display_manager) if mlb_display_modes.get('mlb_recent', True) else None
self.mlb_upcoming = MLBUpcomingManager(self.config, self.display_manager) if mlb_display_modes.get('mlb_upcoming', True) else None
else:
self.mlb_live = None
self.mlb_recent = None
self.mlb_upcoming = None
# Track MLB rotation state
self.mlb_current_team_index = 0
self.mlb_showing_recent = True
self.mlb_favorite_teams = self.config.get('mlb', {}).get('favorite_teams', [])
self.in_mlb_rotation = False
# List of available display modes (adjust order as desired)
self.available_modes = []
@@ -96,6 +117,12 @@ class DisplayController:
if self.nba_recent: self.available_modes.append('nba_recent')
if self.nba_upcoming: self.available_modes.append('nba_upcoming')
# nba_live is handled separately when live games are available
# Add MLB display modes if enabled
if mlb_enabled:
if mlb_display_modes.get('mlb_recent', True): self.available_modes.append('mlb_recent')
if mlb_display_modes.get('mlb_upcoming', True): self.available_modes.append('mlb_upcoming')
# mlb_live is handled separately when live games are available
# Set initial display to first available mode (clock)
self.current_mode_index = 0
@@ -104,19 +131,23 @@ class DisplayController:
self.force_clear = True
self.update_interval = 0.01 # Reduced from 0.1 to 0.01 for smoother scrolling
# Track team-based rotation state for NHL
# Track team-based rotation states
self.nhl_current_team_index = 0
self.nhl_showing_recent = True # True for recent, False for upcoming
self.nhl_showing_recent = True
self.nhl_favorite_teams = self.config.get('nhl_scoreboard', {}).get('favorite_teams', [])
self.in_nhl_rotation = False # Track if we're in NHL rotation
self.in_nhl_rotation = False
# Track team-based rotation state for NBA
self.nba_current_team_index = 0
self.nba_showing_recent = True # True for recent, False for upcoming
self.nba_showing_recent = True
self.nba_favorite_teams = self.config.get('nba_scoreboard', {}).get('favorite_teams', [])
self.in_nba_rotation = False # Track if we're in NBA rotation
self.in_nba_rotation = False
# Update display durations to include NHL and NBA modes
self.mlb_current_team_index = 0
self.mlb_showing_recent = True
self.mlb_favorite_teams = self.config.get('mlb', {}).get('favorite_teams', [])
self.in_mlb_rotation = False
# Update display durations to include all modes
self.display_durations = self.config['display'].get('display_durations', {
'clock': 15,
'weather_current': 15,
@@ -125,13 +156,25 @@ class DisplayController:
'stocks': 45,
'stock_news': 30,
'calendar': 30,
'youtube': 30
'youtube': 30,
'nhl_live': 30,
'nhl_recent': 20,
'nhl_upcoming': 20,
'nba_live': 30,
'nba_recent': 20,
'nba_upcoming': 20,
'mlb_live': 30,
'mlb_recent': 20,
'mlb_upcoming': 20
})
logger.info("DisplayController initialized with display_manager: %s", id(self.display_manager))
logger.info(f"Available display modes: {self.available_modes}")
logger.info(f"NHL Favorite teams: {self.nhl_favorite_teams}")
logger.info(f"NBA Favorite teams: {self.nba_favorite_teams}")
logger.info(f"MLB Favorite teams: {self.mlb_favorite_teams}")
logger.info("NHL managers initialized in %.3f seconds", time.time() - nhl_time)
logger.info("MLB managers initialized in %.3f seconds", time.time() - mlb_time)
def get_current_duration(self) -> int:
"""Get the duration for the current display mode."""
@@ -163,13 +206,18 @@ class DisplayController:
if self.nba_live: self.nba_live.update()
if self.nba_recent: self.nba_recent.update()
if self.nba_upcoming: self.nba_upcoming.update()
# Update MLB managers
if self.mlb_live: self.mlb_live.update()
if self.mlb_recent: self.mlb_recent.update()
if self.mlb_upcoming: self.mlb_upcoming.update()
def _check_live_games(self) -> tuple[bool, str]:
"""
Check if there are any live games available.
Returns:
tuple[bool, str]: (has_live_games, sport_type)
sport_type will be 'nhl' or 'nba' or None
sport_type will be 'nhl', 'nba', 'mlb' or None
"""
# Check NHL live games
if self.nhl_live and self.nhl_live.live_games:
@@ -179,6 +227,10 @@ class DisplayController:
if self.nba_live and self.nba_live.live_games:
return True, 'nba'
# Check MLB live games
if self.mlb_live and self.mlb_live.live_games:
return True, 'mlb'
return False, None
def _get_team_games(self, team: str, sport: str = 'nhl', is_recent: bool = True) -> bool:
@@ -186,7 +238,7 @@ class DisplayController:
Get games for a specific team and update the current game.
Args:
team: Team abbreviation
sport: 'nhl' or 'nba'
sport: 'nhl', 'nba', or 'mlb'
is_recent: Whether to look for recent or upcoming games
Returns:
bool: True if games were found and set
@@ -217,206 +269,167 @@ class DisplayController:
if game["home_abbr"] == team or game["away_abbr"] == team:
self.nba_upcoming.current_game = game
return True
elif sport == 'mlb':
if is_recent and self.mlb_recent:
# Find recent games for this team
for game in self.mlb_recent.recent_games:
if game['home_team'] == team or game['away_team'] == team:
self.mlb_recent.current_game = game
return True
elif not is_recent and self.mlb_upcoming:
# Find upcoming games for this team
for game in self.mlb_upcoming.upcoming_games:
if game['home_team'] == team or game['away_team'] == team:
self.mlb_upcoming.current_game = game
return True
return False
def _has_team_games(self, sport: str = 'nhl') -> bool:
"""
Check if there are any games available for favorite teams.
Args:
sport: 'nhl' or 'nba'
Returns:
bool: True if games are available
"""
"""Check if there are any games for favorite teams."""
if sport == 'nhl':
favorite_teams = self.nhl_favorite_teams
recent_manager = self.nhl_recent
upcoming_manager = self.nhl_upcoming
else:
favorite_teams = self.nba_favorite_teams
recent_manager = self.nba_recent
upcoming_manager = self.nba_upcoming
if not favorite_teams:
return False
# Check recent games
if recent_manager and recent_manager.games_list:
for game in recent_manager.games_list:
if game["home_abbr"] in favorite_teams or game["away_abbr"] in favorite_teams:
return True
# Check upcoming games
if upcoming_manager and upcoming_manager.games_list:
for game in upcoming_manager.games_list:
if game["home_abbr"] in favorite_teams or game["away_abbr"] in favorite_teams:
return True
return bool(self.nhl_favorite_teams and (self.nhl_recent or self.nhl_upcoming))
elif sport == 'nba':
return bool(self.nba_favorite_teams and (self.nba_recent or self.nba_upcoming))
elif sport == 'mlb':
return bool(self.mlb_favorite_teams and self.mlb_live)
return False
def _rotate_team_games(self, sport: str = 'nhl') -> None:
"""
Rotate through games for favorite teams.
Args:
sport: 'nhl' or 'nba'
"""
"""Rotate through games for favorite teams."""
if sport == 'nhl':
current_team_index = self.nhl_current_team_index
showing_recent = self.nhl_showing_recent
favorite_teams = self.nhl_favorite_teams
in_rotation = self.in_nhl_rotation
else:
current_team_index = self.nba_current_team_index
showing_recent = self.nba_showing_recent
favorite_teams = self.nba_favorite_teams
in_rotation = self.in_nba_rotation
if not self._has_team_games('nhl'): return
if not favorite_teams:
return
# Try to find games for current team
current_team = self.nhl_favorite_teams[self.nhl_current_team_index]
found_games = self._get_team_games(current_team, 'nhl', self.nhl_showing_recent)
# Try to find games for current team
team = favorite_teams[current_team_index]
found_games = self._get_team_games(team, sport, showing_recent)
if not found_games:
# If no games found for current team, try next team
current_team_index = (current_team_index + 1) % len(favorite_teams)
if sport == 'nhl':
self.nhl_current_team_index = current_team_index
else:
self.nba_current_team_index = current_team_index
if not found_games:
# Try opposite type (recent/upcoming) for same team
self.nhl_showing_recent = not self.nhl_showing_recent
found_games = self._get_team_games(current_team, 'nhl', self.nhl_showing_recent)
if not found_games:
# Move to next team
self.nhl_current_team_index = (self.nhl_current_team_index + 1) % len(self.nhl_favorite_teams)
self.nhl_showing_recent = True # Reset to recent games for next team
# If we've tried all teams, switch between recent and upcoming
if current_team_index == 0:
if sport == 'nhl':
self.nhl_showing_recent = not self.nhl_showing_recent
else:
self.nba_showing_recent = not self.nba_showing_recent
showing_recent = not showing_recent
# Try again with new team
team = favorite_teams[current_team_index]
found_games = self._get_team_games(team, sport, showing_recent)
elif sport == 'nba':
if not self._has_team_games('nba'): return
if found_games:
# Set the appropriate display mode
if sport == 'nhl':
self.current_display_mode = 'nhl_recent' if showing_recent else 'nhl_upcoming'
self.in_nhl_rotation = True
else:
self.current_display_mode = 'nba_recent' if showing_recent else 'nba_upcoming'
self.in_nba_rotation = True
else:
# No games found for any team, exit rotation
if sport == 'nhl':
self.in_nhl_rotation = False
else:
self.in_nba_rotation = False
# Try to find games for current team
current_team = self.nba_favorite_teams[self.nba_current_team_index]
found_games = self._get_team_games(current_team, 'nba', self.nba_showing_recent)
if not found_games:
# Try opposite type (recent/upcoming) for same team
self.nba_showing_recent = not self.nba_showing_recent
found_games = self._get_team_games(current_team, 'nba', self.nba_showing_recent)
if not found_games:
# Move to next team
self.nba_current_team_index = (self.nba_current_team_index + 1) % len(self.nba_favorite_teams)
self.nba_showing_recent = True # Reset to recent games for next team
elif sport == 'mlb':
if not self._has_team_games('mlb'): return
# Try to find games for current team
current_team = self.mlb_favorite_teams[self.mlb_current_team_index]
found_games = self._get_team_games(current_team, 'mlb', self.mlb_showing_recent)
if not found_games:
# Try opposite type (recent/upcoming) for same team
self.mlb_showing_recent = not self.mlb_showing_recent
found_games = self._get_team_games(current_team, 'mlb', self.mlb_showing_recent)
if not found_games:
# Move to next team
self.mlb_current_team_index = (self.mlb_current_team_index + 1) % len(self.mlb_favorite_teams)
self.mlb_showing_recent = True # Reset to recent games for next team
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
"""Main display loop."""
try:
while True:
current_time = time.time()
# Update data for all modules
self._update_modules()
# Check for live games
# Check for live games first
has_live_games, sport_type = self._check_live_games()
# If we have live games, cycle through them
if has_live_games:
# Check if it's time to switch live games
if current_time - self.last_switch > self.get_current_duration():
# Switch between NHL and NBA live games if both are available
if sport_type == 'nhl' and self.nhl_live and self.nba_live and self.nba_live.live_games:
sport_type = 'nba'
self.last_switch = current_time
self.force_clear = True
elif sport_type == 'nba' and self.nba_live and self.nhl_live and self.nhl_live.live_games:
sport_type = 'nhl'
self.last_switch = current_time
self.force_clear = True
# Display the current live game
# Handle live game display
if sport_type == 'nhl' and self.nhl_live:
self.nhl_live.update() # Force update to get latest data
self.nhl_live.display(force_clear=self.force_clear)
self.nhl_live.display_games(self.force_clear)
self.current_display_mode = 'nhl_live'
elif sport_type == 'nba' and self.nba_live:
self.nba_live.update() # Force update to get latest data
self.nba_live.display(force_clear=self.force_clear)
self.nba_live.display_games(self.force_clear)
self.current_display_mode = 'nba_live'
elif sport_type == 'mlb' and self.mlb_live:
self.mlb_live.display_games(self.force_clear)
self.current_display_mode = 'mlb_live'
else:
# Regular display rotation
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]
self.last_switch = current_time
self.force_clear = True
# Reset rotation flags when switching modes
if not self.current_display_mode.startswith('nhl_'):
self.in_nhl_rotation = False
if not self.current_display_mode.startswith('nba_'):
self.in_nba_rotation = False
if not self.current_display_mode.startswith('mlb_'):
self.in_mlb_rotation = False
self.force_clear = False
continue # Skip the rest of the loop to stay on live games
# Only proceed with mode switching if no live games
if current_time - self.last_switch > self.get_current_duration():
# No live games, continue with regular rotation
# If we're currently on calendar, advance to next event before switching modes
if self.current_display_mode == 'calendar' and self.calendar:
self.calendar.advance_event()
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 to: {self.current_display_mode}")
self.force_clear = True
self.last_switch = current_time
# Display current mode frame (only for non-live modes)
try:
# Handle current display mode
if self.current_display_mode == 'clock' and self.clock:
self.clock.display_time(force_clear=self.force_clear)
elif self.current_display_mode == 'weather_current' and self.weather:
self.weather.display_weather(force_clear=self.force_clear)
elif self.current_display_mode == 'weather_hourly' and self.weather:
self.weather.display_hourly_forecast(force_clear=self.force_clear)
elif self.current_display_mode == 'weather_daily' and self.weather:
self.weather.display_daily_forecast(force_clear=self.force_clear)
self.clock.display_time()
elif self.current_display_mode.startswith('weather_') and self.weather:
mode = self.current_display_mode.split('_')[1]
self.weather.display_weather(mode)
elif self.current_display_mode == 'stocks' and self.stocks:
self.stocks.display_stocks(force_clear=self.force_clear)
done = self.stocks.display_stocks(self.force_clear)
if done: self.force_clear = True
elif self.current_display_mode == 'stock_news' and self.news:
self.news.display_news()
done = self.news.display_news(self.force_clear)
if done: self.force_clear = True
elif self.current_display_mode == 'calendar' and self.calendar:
# Update calendar data if needed
self.calendar.update(current_time)
# Always display the calendar, with force_clear only on mode switch
self.calendar.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 == 'nba_recent' and self.nba_recent:
self.nba_recent.display(force_clear=self.force_clear)
elif self.current_display_mode == 'nba_upcoming' and self.nba_upcoming:
self.nba_upcoming.display(force_clear=self.force_clear)
self.calendar.display()
elif self.current_display_mode == 'youtube' and self.youtube:
self.youtube.display(force_clear=self.force_clear)
except Exception as e:
logger.error(f"Error updating display for mode {self.current_display_mode}: {e}", exc_info=True)
continue
self.youtube.display()
elif self.current_display_mode.startswith('nhl_'):
self._handle_nhl_display()
elif self.current_display_mode.startswith('nba_'):
self._handle_nba_display()
elif self.current_display_mode.startswith('mlb_'):
self._handle_mlb_display()
# Update modules periodically
self._update_modules()
# Reset force clear flag
self.force_clear = False
# Small delay to prevent excessive CPU usage
time.sleep(self.update_interval)
except KeyboardInterrupt:
logger.info("Display controller stopped by user")
logger.info("Display loop interrupted by user")
except Exception as e:
logger.error(f"Error in display controller: {e}", exc_info=True)
logger.error(f"Error in display loop: {e}", exc_info=True)
finally:
self.display_manager.cleanup()
self.display_manager.clear()
def _handle_mlb_display(self):
"""Handle MLB display modes."""
if self.current_display_mode == 'mlb_live' and self.mlb_live:
self.mlb_live.display(self.force_clear)
elif self.current_display_mode == 'mlb_recent' and self.mlb_recent:
self.mlb_recent.display(self.force_clear)
elif self.current_display_mode == 'mlb_upcoming' and self.mlb_upcoming:
self.mlb_upcoming.display(self.force_clear)
def main():
controller = DisplayController()

478
src/mlb_manager.py Normal file
View File

@@ -0,0 +1,478 @@
import time
import logging
import requests
import json
from typing import Dict, Any, List, Optional
from datetime import datetime, timedelta
import os
from PIL import Image, ImageDraw, ImageFont
import numpy as np
from .cache_manager import CacheManager
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# Get logger
logger = logging.getLogger(__name__)
class BaseMLBManager:
"""Base class for MLB managers with common functionality."""
def __init__(self, config: Dict[str, Any], display_manager):
self.config = config
self.display_manager = display_manager
self.mlb_config = config.get('mlb', {})
self.favorite_teams = self.mlb_config.get('favorite_teams', [])
self.cache_manager = CacheManager()
# Logo handling
self.logo_dir = self.mlb_config.get('logo_dir', os.path.join('assets', 'sports', 'mlb_logos'))
if not os.path.exists(self.logo_dir):
logger.warning(f"MLB logos directory not found: {self.logo_dir}")
try:
os.makedirs(self.logo_dir, exist_ok=True)
logger.info(f"Created MLB logos directory: {self.logo_dir}")
except Exception as e:
logger.error(f"Failed to create MLB logos directory: {e}")
# Set up session with retry logic
self.session = requests.Session()
retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
self.session.mount("http://", adapter)
self.session.mount("https://", adapter)
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
def _get_team_logo(self, team_abbr: str) -> Optional[Image.Image]:
"""Get team logo from the configured directory."""
try:
logo_path = os.path.join(self.logo_dir, f"{team_abbr}.png")
if os.path.exists(logo_path):
return Image.open(logo_path)
else:
logger.warning(f"Logo not found for team {team_abbr}")
return None
except Exception as e:
logger.error(f"Error loading logo for team {team_abbr}: {e}")
return None
def _draw_base_indicators(self, draw: ImageDraw.Draw, bases_occupied: List[bool], center_x: int, y: int) -> None:
"""Draw base indicators on the display."""
base_size = 4
base_spacing = 6
# Draw diamond outline
diamond_points = [
(center_x, y), # Home
(center_x - base_spacing, y - base_spacing), # First
(center_x, y - 2 * base_spacing), # Second
(center_x + base_spacing, y - base_spacing) # Third
]
draw.polygon(diamond_points, outline=(255, 255, 255))
# Draw occupied bases
for i, occupied in enumerate(bases_occupied):
if occupied:
x = diamond_points[i+1][0] - base_size//2
y = diamond_points[i+1][1] - base_size//2
draw.ellipse([x, y, x + base_size, y + base_size], fill=(255, 255, 255))
def _create_game_display(self, game_data: Dict[str, Any]) -> Image.Image:
"""Create a display image for an MLB game with team logos, score, and game state."""
width = self.display_manager.matrix.width
height = self.display_manager.matrix.height
image = Image.new('RGB', (width, height), color=(0, 0, 0))
draw = ImageDraw.Draw(image)
# Load team logos
away_logo = self._get_team_logo(game_data['away_team'])
home_logo = self._get_team_logo(game_data['home_team'])
# Logo size and positioning
logo_size = (16, 16)
if away_logo and home_logo:
away_logo = away_logo.resize(logo_size, Image.Resampling.LANCZOS)
home_logo = home_logo.resize(logo_size, Image.Resampling.LANCZOS)
# Position logos on left and right sides
image.paste(away_logo, (2, height//2 - logo_size[1]//2), away_logo)
image.paste(home_logo, (width - logo_size[0] - 2, height//2 - logo_size[1]//2), home_logo)
# Draw scores
score_y = height//2 - 4
away_score = str(game_data['away_score'])
home_score = str(game_data['home_score'])
# Use small font for scores
draw.text((20, score_y), away_score, fill=(255, 255, 255), font=self.display_manager.small_font)
draw.text((width - 20 - len(home_score)*4, score_y), home_score, fill=(255, 255, 255), font=self.display_manager.small_font)
# Draw game status
if game_data['status'] == 'live':
# Draw inning indicator at top
inning = game_data['inning']
inning_half = '' if game_data['inning_half'] == 'top' else ''
inning_text = f"{inning_half}{inning}"
# Center the inning text
inning_bbox = draw.textbbox((0, 0), inning_text, font=self.display_manager.small_font)
inning_width = inning_bbox[2] - inning_bbox[0]
inning_x = (width - inning_width) // 2
draw.text((inning_x, 2), inning_text, fill=(255, 255, 255), font=self.display_manager.small_font)
# Draw base indicators
self._draw_base_indicators(draw, game_data['bases_occupied'], width//2, 12)
# Draw count (balls-strikes) at bottom
count_text = f"{game_data['balls']}-{game_data['strikes']}"
count_bbox = draw.textbbox((0, 0), count_text, font=self.display_manager.small_font)
count_width = count_bbox[2] - count_bbox[0]
count_x = (width - count_width) // 2
draw.text((count_x, height - 10), count_text, fill=(255, 255, 255), font=self.display_manager.small_font)
else:
# Show game time for upcoming games or "Final" for completed games
status_text = "Final" if game_data['status'] == 'final' else self._format_game_time(game_data['start_time'])
status_bbox = draw.textbbox((0, 0), status_text, font=self.display_manager.small_font)
status_width = status_bbox[2] - status_bbox[0]
status_x = (width - status_width) // 2
draw.text((status_x, 2), status_text, fill=(255, 255, 255), font=self.display_manager.small_font)
return image
def _format_game_time(self, game_time: str) -> str:
"""Format game time for display."""
try:
dt = datetime.fromisoformat(game_time.replace('Z', '+00:00'))
return dt.strftime("%I:%M %p")
except Exception as e:
logger.error(f"Error formatting game time: {e}")
return "TBD"
def _fetch_mlb_api_data(self) -> Dict[str, Any]:
"""Fetch MLB game data from the API."""
try:
# MLB Stats API endpoint for schedule
today = datetime.now().strftime('%Y-%m-%d')
url = f"https://statsapi.mlb.com/api/v1/schedule/games/{today}"
response = self.session.get(url, headers=self.headers, timeout=10)
response.raise_for_status()
data = response.json()
games = {}
for date in data.get('dates', []):
for game in date.get('games', []):
game_id = game['gamePk']
# Get detailed game data for live games
if game['status']['abstractGameState'] == 'Live':
game_url = f"https://statsapi.mlb.com/api/v1/game/{game_id}/linescore"
game_response = self.session.get(game_url, headers=self.headers, timeout=10)
game_response.raise_for_status()
game_data = game_response.json()
# Extract inning, count, and base runner info
inning = game_data.get('currentInning', 1)
inning_half = game_data.get('inningHalf', '').lower()
balls = game_data.get('balls', 0)
strikes = game_data.get('strikes', 0)
bases_occupied = [
game_data.get('offense', {}).get('first', False),
game_data.get('offense', {}).get('second', False),
game_data.get('offense', {}).get('third', False)
]
else:
# Default values for non-live games
inning = 1
inning_half = 'top'
balls = 0
strikes = 0
bases_occupied = [False, False, False]
games[game_id] = {
'away_team': game['teams']['away']['team']['abbreviation'],
'home_team': game['teams']['home']['team']['abbreviation'],
'away_score': game['teams']['away']['score'],
'home_score': game['teams']['home']['score'],
'status': game['status']['abstractGameState'].lower(),
'inning': inning,
'inning_half': inning_half,
'balls': balls,
'strikes': strikes,
'bases_occupied': bases_occupied,
'start_time': game['gameDate']
}
return games
except Exception as e:
logger.error(f"Error fetching MLB data: {e}")
return {}
class MBLLiveManager(BaseMLBManager):
"""Manager for live MLB games."""
def __init__(self, config: Dict[str, Any], display_manager):
super().__init__(config, display_manager)
self.update_interval = self.mlb_config.get('live_update_interval', 20) # 20 seconds for live games
self.no_data_interval = 300 # 5 minutes when no live games
self.last_update = 0
self.logger.info("Initialized MLB Live Manager")
self.live_games = [] # List to store all live games
self.current_game_index = 0 # Index to track which game to show
self.last_game_switch = 0 # Track when we last switched games
self.game_display_duration = self.mlb_config.get('live_game_duration', 30) # Display each live game for 30 seconds
self.last_display_update = 0 # Track when we last updated the display
self.last_log_time = 0
self.log_interval = 300 # Only log status every 5 minutes
def update(self):
"""Update live game data."""
current_time = time.time()
# Use longer interval if no game data
interval = self.no_data_interval if not self.live_games else self.update_interval
if current_time - self.last_update >= interval:
self.last_update = current_time
# Fetch live game data from MLB API
games = self._fetch_mlb_api_data()
if games:
# Find all live games involving favorite teams
new_live_games = []
for game in games.values():
if game['status'] == 'live':
if not self.favorite_teams or (
game['home_team'] in self.favorite_teams or
game['away_team'] in self.favorite_teams
):
new_live_games.append(game)
# Only log if there's a change in games or enough time has passed
should_log = (
current_time - self.last_log_time >= self.log_interval or
len(new_live_games) != len(self.live_games) or
not self.live_games # Log if we had no games before
)
if should_log:
if new_live_games:
logger.info(f"[MLB] Found {len(new_live_games)} live games")
for game in new_live_games:
logger.info(f"[MLB] Live game: {game['away_team']} vs {game['home_team']} - {game['inning_half']}{game['inning']}, {game['balls']}-{game['strikes']}")
else:
logger.info("[MLB] No live games found")
self.last_log_time = current_time
if new_live_games:
# Update the current game with the latest data
for new_game in new_live_games:
if self.current_game and (
(new_game['home_team'] == self.current_game['home_team'] and
new_game['away_team'] == self.current_game['away_team']) or
(new_game['home_team'] == self.current_game['away_team'] and
new_game['away_team'] == self.current_game['home_team'])
):
self.current_game = new_game
break
# Only update the games list if we have new games
if not self.live_games or set(game['away_team'] + game['home_team'] for game in new_live_games) != set(game['away_team'] + game['home_team'] for game in self.live_games):
self.live_games = new_live_games
# If we don't have a current game or it's not in the new list, start from the beginning
if not self.current_game or self.current_game not in self.live_games:
self.current_game_index = 0
self.current_game = self.live_games[0]
self.last_game_switch = current_time
# Always update display when we have new data, but limit to once per second
if current_time - self.last_display_update >= 1.0:
self.display(force_clear=True)
self.last_display_update = current_time
else:
# No live games found
self.live_games = []
self.current_game = None
# Check if it's time to switch games
if len(self.live_games) > 1 and (current_time - self.last_game_switch) >= self.game_display_duration:
self.current_game_index = (self.current_game_index + 1) % len(self.live_games)
self.current_game = self.live_games[self.current_game_index]
self.last_game_switch = current_time
# Force display update when switching games
self.display(force_clear=True)
self.last_display_update = current_time
def display(self, force_clear: bool = False):
"""Display live game information."""
if not self.current_game:
return
try:
# Create and display the game image
game_image = self._create_game_display(self.current_game)
image_array = np.array(game_image)
self.display_manager.update_display(image_array)
except Exception as e:
logger.error(f"[MLB] Error displaying live game: {e}", exc_info=True)
class MLBRecentManager(BaseMLBManager):
"""Manager for recently completed MLB games."""
def __init__(self, config: Dict[str, Any], display_manager):
super().__init__(config, display_manager)
self.recent_games = []
self.current_game_index = 0
self.last_update = 0
self.update_interval = self.mlb_config.get('recent_update_interval', 3600) # 1 hour for recent games
self.recent_hours = self.mlb_config.get('recent_game_hours', 48)
self.last_game_switch = 0
self.game_display_duration = 20 # Display each game for 20 seconds
self.last_warning_time = 0
self.warning_cooldown = 300 # Only show warning every 5 minutes
logger.info(f"Initialized MLBRecentManager with {len(self.favorite_teams)} favorite teams")
def update(self):
"""Update recent games data."""
current_time = time.time()
if current_time - self.last_update < self.update_interval:
return
try:
# Fetch data from MLB API
games = self._fetch_mlb_api_data()
if games:
# Process games
new_recent_games = []
now = datetime.now()
recent_cutoff = now - timedelta(hours=self.recent_hours)
for game in games.values():
game_time = datetime.fromisoformat(game['start_time'].replace('Z', '+00:00'))
if game['status'] == 'final' and game_time >= recent_cutoff:
new_recent_games.append(game)
# Filter for favorite teams
new_team_games = [game for game in new_recent_games
if game['home_team'] in self.favorite_teams or
game['away_team'] in self.favorite_teams]
if new_team_games:
logger.info(f"[MLB] Found {len(new_team_games)} recent games for favorite teams")
self.recent_games = new_team_games
if not self.current_game:
self.current_game = self.recent_games[0]
else:
logger.info("[MLB] No recent games found for favorite teams")
self.recent_games = []
self.current_game = None
self.last_update = current_time
except Exception as e:
logger.error(f"[MLB] Error updating recent games: {e}", exc_info=True)
def display(self, force_clear: bool = False):
"""Display recent games."""
if not self.recent_games:
current_time = time.time()
if current_time - self.last_warning_time > self.warning_cooldown:
logger.info("[MLB] No recent games to display")
self.last_warning_time = current_time
return # Skip display update entirely
try:
current_time = time.time()
# Check if it's time to switch games
if current_time - self.last_game_switch >= self.game_display_duration:
# Move to next game
self.current_game_index = (self.current_game_index + 1) % len(self.recent_games)
self.current_game = self.recent_games[self.current_game_index]
self.last_game_switch = current_time
force_clear = True # Force clear when switching games
# Create and display the game image
game_image = self._create_game_display(self.current_game)
image_array = np.array(game_image)
self.display_manager.update_display(image_array)
except Exception as e:
logger.error(f"[MLB] Error displaying recent game: {e}", exc_info=True)
class MLBUpcomingManager(BaseMLBManager):
"""Manager for upcoming MLB games."""
def __init__(self, config: Dict[str, Any], display_manager):
super().__init__(config, display_manager)
self.upcoming_games = []
self.current_game_index = 0
self.last_update = 0
self.update_interval = self.mlb_config.get('upcoming_update_interval', 3600) # 1 hour for upcoming games
self.last_warning_time = 0
self.warning_cooldown = 300 # Only show warning every 5 minutes
logger.info(f"Initialized MLBUpcomingManager with {len(self.favorite_teams)} favorite teams")
def update(self):
"""Update upcoming games data."""
current_time = time.time()
if current_time - self.last_update < self.update_interval:
return
try:
# Fetch data from MLB API
games = self._fetch_mlb_api_data()
if games:
# Process games
new_upcoming_games = []
now = datetime.now()
upcoming_cutoff = now + timedelta(hours=24)
for game in games.values():
game_time = datetime.fromisoformat(game['start_time'].replace('Z', '+00:00'))
if game['status'] == 'preview' and game_time <= upcoming_cutoff:
new_upcoming_games.append(game)
# Filter for favorite teams
new_team_games = [game for game in new_upcoming_games
if game['home_team'] in self.favorite_teams or
game['away_team'] in self.favorite_teams]
if new_team_games:
logger.info(f"[MLB] Found {len(new_team_games)} upcoming games for favorite teams")
self.upcoming_games = new_team_games
if not self.current_game:
self.current_game = self.upcoming_games[0]
else:
logger.info("[MLB] No upcoming games found for favorite teams")
self.upcoming_games = []
self.current_game = None
self.last_update = current_time
except Exception as e:
logger.error(f"[MLB] Error updating upcoming games: {e}", exc_info=True)
def display(self, force_clear: bool = False):
"""Display upcoming games."""
if not self.upcoming_games:
current_time = time.time()
if current_time - self.last_warning_time > self.warning_cooldown:
logger.info("[MLB] No upcoming games to display")
self.last_warning_time = current_time
return # Skip display update entirely
try:
# Create and display the game image
game_image = self._create_game_display(self.current_game)
image_array = np.array(game_image)
self.display_manager.update_display(image_array)
# Move to next game
self.current_game_index = (self.current_game_index + 1) % len(self.upcoming_games)
self.current_game = self.upcoming_games[self.current_game_index]
except Exception as e:
logger.error(f"[MLB] Error displaying upcoming game: {e}", exc_info=True)