mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-06-23 20:58:37 +00:00
* feat(config): optimize memory usage to prevent OOM killer - Reduce brightness from 95 to 50 to lower power consumption - Reduce refresh rate from 120Hz to 100Hz to reduce CPU/memory pressure - Reduce background service workers from 3 to 1 per manager - Change hardware mapping from adafruit-hat-pwm to adafruit-hat - Expected memory savings: ~700MB reduction in background service usage - Addresses SIGKILL errors caused by memory exhaustion on Raspberry Pi Fixes: OOM killer terminating ledmatrix.service with status=9/KILL * revert display brightness * refactor(background-service): hardcode optimized settings and remove config blocks - Hardcode background service to 1 worker in all managers - Remove background_service config blocks from template - Simplify configuration for users - no need to adjust system settings - Memory optimization: ~700MB reduction in background service usage - Settings: 1 worker, 30s timeout, 3 retries (hardcoded) Files updated: - src/base_classes/sports.py - src/leaderboard_manager.py - src/odds_ticker_manager.py - src/soccer_managers.py - src/milb_manager.py - config/config.template.json This prevents OOM killer errors by reducing memory usage from 15 background threads to 5 threads total across all managers. * remove fallback in case of background service failure
194 lines
8.8 KiB
Python
194 lines
8.8 KiB
Python
import time
|
|
import logging
|
|
import requests
|
|
from typing import Dict, Any, Optional
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
from PIL import Image, ImageFont
|
|
from src.display_manager import DisplayManager
|
|
from src.cache_manager import CacheManager
|
|
from src.base_classes.basketball import Basketball, BasketballLive
|
|
from src.base_classes.sports import SportsRecent, SportsUpcoming
|
|
import pytz
|
|
|
|
# Import the API counter function from web interface
|
|
try:
|
|
from web_interface_v2 import increment_api_counter
|
|
except ImportError:
|
|
# Fallback if web interface is not available
|
|
def increment_api_counter(kind: str, count: int = 1):
|
|
pass
|
|
|
|
# Constants
|
|
ESPN_NBA_SCOREBOARD_URL = "https://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard"
|
|
|
|
class BaseNBAManager(Basketball):
|
|
"""Base class for NBA managers with common functionality."""
|
|
# Class variables for warning tracking
|
|
_no_data_warning_logged = False
|
|
_last_warning_time = 0
|
|
_warning_cooldown = 60 # Only log warnings once per minute
|
|
_last_log_times = {}
|
|
_shared_data = None
|
|
_last_shared_update = 0
|
|
|
|
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager):
|
|
self.logger = logging.getLogger('NBA') # Changed logger name
|
|
super().__init__(config=config, display_manager=display_manager, cache_manager=cache_manager, logger=self.logger, sport_key="nba")
|
|
|
|
# Check display modes to determine what data to fetch
|
|
display_modes = self.mode_config.get("display_modes", {})
|
|
self.recent_enabled = display_modes.get("nba_recent", False)
|
|
self.upcoming_enabled = display_modes.get("nba_upcoming", False)
|
|
self.live_enabled = display_modes.get("nba_live", False)
|
|
|
|
self.logger.info(f"Initialized NBA manager with display dimensions: {self.display_width}x{self.display_height}")
|
|
self.logger.info(f"Logo directory: {self.logo_dir}")
|
|
self.logger.info(f"Display modes - Recent: {self.recent_enabled}, Upcoming: {self.upcoming_enabled}, Live: {self.live_enabled}")
|
|
self.league = "nba"
|
|
|
|
def _fetch_nba_api_data(self, use_cache: bool = True) -> Optional[Dict]:
|
|
"""
|
|
Fetches the full season schedule for NBA using background threading.
|
|
Returns cached data immediately if available, otherwise starts background fetch.
|
|
"""
|
|
now = datetime.now(pytz.utc)
|
|
season_year = now.year
|
|
if now.month < 7:
|
|
season_year = now.year - 1
|
|
datestring = f"{season_year}1001-{season_year+1}0701"
|
|
cache_key = f"{self.sport_key}_schedule_{season_year}"
|
|
|
|
# Check cache first
|
|
if use_cache:
|
|
cached_data = self.cache_manager.get(cache_key)
|
|
if cached_data:
|
|
# Validate cached data structure
|
|
if isinstance(cached_data, dict) and 'events' in cached_data:
|
|
self.logger.info(f"Using cached schedule for {season_year}")
|
|
return cached_data
|
|
elif isinstance(cached_data, list):
|
|
# Handle old cache format (list of events)
|
|
self.logger.info(f"Using cached schedule for {season_year} (legacy format)")
|
|
return {'events': cached_data}
|
|
else:
|
|
self.logger.warning(f"Invalid cached data format for {season_year}: {type(cached_data)}")
|
|
# Clear invalid cache
|
|
self.cache_manager.clear_cache(cache_key)
|
|
|
|
# Start background fetch
|
|
self.logger.info(f"Starting background fetch for {season_year} season schedule...")
|
|
|
|
def fetch_callback(result):
|
|
"""Callback when background fetch completes."""
|
|
if result.success:
|
|
self.logger.info(f"Background fetch completed for {season_year}: {len(result.data.get('events'))} events")
|
|
else:
|
|
self.logger.error(f"Background fetch failed for {season_year}: {result.error}")
|
|
|
|
# Clean up request tracking
|
|
if season_year in self.background_fetch_requests:
|
|
del self.background_fetch_requests[season_year]
|
|
|
|
# Get background service configuration
|
|
background_config = self.mode_config.get("background_service", {})
|
|
timeout = background_config.get("request_timeout", 30)
|
|
max_retries = background_config.get("max_retries", 3)
|
|
priority = background_config.get("priority", 2)
|
|
|
|
# Submit background fetch request
|
|
request_id = self.background_service.submit_fetch_request(
|
|
sport="nba",
|
|
year=season_year,
|
|
url=ESPN_NBA_SCOREBOARD_URL,
|
|
cache_key=cache_key,
|
|
params={"dates": datestring, "limit": 1000},
|
|
headers=self.headers,
|
|
timeout=timeout,
|
|
max_retries=max_retries,
|
|
priority=priority,
|
|
callback=fetch_callback
|
|
)
|
|
|
|
# Track the request
|
|
self.background_fetch_requests[season_year] = request_id
|
|
|
|
# For immediate response, try to get partial data
|
|
partial_data = self._get_weeks_data()
|
|
if partial_data:
|
|
return partial_data
|
|
|
|
return None
|
|
|
|
def _fetch_data(self) -> Optional[Dict]:
|
|
"""Fetch data using shared data mechanism or direct fetch for live."""
|
|
if isinstance(self, NBALiveManager):
|
|
# Live games should fetch only current games, not entire season
|
|
return self._fetch_todays_games()
|
|
else:
|
|
# Recent and Upcoming managers should use cached season data
|
|
return self._fetch_nba_api_data(use_cache=True)
|
|
|
|
class NBALiveManager(BaseNBAManager, BasketballLive):
|
|
"""Manager for live NBA games."""
|
|
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager):
|
|
super().__init__(config, display_manager, cache_manager)
|
|
self.logger = logging.getLogger('NBALiveManager') # Changed logger name
|
|
|
|
if self.test_mode:
|
|
# More detailed test game for NBA
|
|
self.current_game = {
|
|
"id": "test001",
|
|
"home_abbr": "LAL", "home_id": "123", "away_abbr": "GS", "away_id":"asdf",
|
|
"home_score": "21", "away_score": "17",
|
|
"period": 3, "period_text": "Q3", "clock": "5:24",
|
|
"home_logo_path": Path(self.logo_dir, "LAL.png"),
|
|
"away_logo_path": Path(self.logo_dir, "GS.png"),
|
|
"is_live": True, "is_final": False, "is_upcoming": False, "is_halftime": False,
|
|
}
|
|
self.live_games = [self.current_game]
|
|
self.logger.info("Initialized NBALiveManager with test game: BUF vs KC")
|
|
else:
|
|
self.logger.info(" Initialized NBALiveManager in live mode")
|
|
|
|
|
|
class NBARecentManager(BaseNBAManager, SportsRecent):
|
|
"""Manager for recently completed NBA games."""
|
|
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager):
|
|
super().__init__(config, display_manager, cache_manager)
|
|
self.logger = logging.getLogger('NBARecentManager') # Changed logger name
|
|
self.logger.info(f"Initialized NBARecentManager with {len(self.favorite_teams)} favorite teams")
|
|
|
|
class NBAUpcomingManager(BaseNBAManager, SportsUpcoming):
|
|
"""Manager for upcoming NBA games."""
|
|
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager):
|
|
super().__init__(config, display_manager, cache_manager)
|
|
self.logger = logging.getLogger('NBAUpcomingManager') # Changed logger name
|
|
self.logger.info(f"Initialized NBAUpcomingManager with {len(self.favorite_teams)} favorite teams")
|
|
|
|
"""Display upcoming games."""
|
|
if not self.upcoming_games:
|
|
return
|
|
|
|
try:
|
|
current_time = time.time()
|
|
|
|
# Check if it's time to switch games
|
|
if len(self.upcoming_games) > 1 and 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.upcoming_games)
|
|
self.current_game = self.upcoming_games[self.current_game_index]
|
|
self.last_game_switch = current_time
|
|
force_clear = True
|
|
|
|
# Log team switching
|
|
if self.current_game:
|
|
away_abbr = self.current_game.get('away_abbr', 'UNK')
|
|
home_abbr = self.current_game.get('home_abbr', 'UNK')
|
|
self.logger.info(f"[NBA Upcoming] Showing {away_abbr} vs {home_abbr}")
|
|
|
|
# Draw the scorebug layout
|
|
self._draw_scorebug_layout(self.current_game, force_clear)
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"[NBA] Error displaying upcoming game: {e}", exc_info=True) |