From 1922d5e3b0ece3595bb5f46fe1ee77f036dbc716 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 17 Jul 2025 11:12:45 -0500 Subject: [PATCH] fixed clock / timezone configuration discrepancies --- src/clock.py | 47 ++++++------------------- src/nba_managers.py | 7 ++-- src/ncaa_fb_managers.py | 13 +++++-- src/ncaam_basketball_managers.py | 17 ++++++--- src/nfl_managers.py | 23 ++++++++---- src/nhl_managers.py | 60 ++++++++++++++++++-------------- src/soccer_managers.py | 20 ++++++----- 7 files changed, 102 insertions(+), 85 deletions(-) diff --git a/src/clock.py b/src/clock.py index 1a53cc98..88f0b16c 100644 --- a/src/clock.py +++ b/src/clock.py @@ -30,43 +30,18 @@ class Clock: } def _get_timezone(self) -> pytz.timezone: - """Get timezone based on location or config.""" - # First try to use timezone from config if it exists - if 'timezone' in self.config: - try: - return pytz.timezone(self.config['timezone']) - except pytz.exceptions.UnknownTimeZoneError: - print(f"Warning: Invalid timezone in config: {self.config['timezone']}") - - # If no timezone in config or it's invalid, try to determine from location + """Get timezone from the config file.""" + config_timezone = self.config_manager.get_timezone() try: - from timezonefinder import TimezoneFinder - from geopy.geocoders import Nominatim - from geopy.exc import GeocoderTimedOut - - # Get coordinates for the location - geolocator = Nominatim(user_agent="led_matrix_clock") - location_str = f"{self.location['city']}, {self.location['state']}, {self.location['country']}" - - try: - location = geolocator.geocode(location_str, timeout=5) # 5 second timeout - if location: - # Find timezone from coordinates - tf = TimezoneFinder() - timezone_str = tf.timezone_at(lng=location.longitude, lat=location.latitude) - if timezone_str: - return pytz.timezone(timezone_str) - except GeocoderTimedOut: - print("Warning: Timeout while looking up location coordinates") - except Exception as e: - print(f"Warning: Error finding timezone from location: {e}") - - except Exception as e: - print(f"Warning: Error importing geolocation libraries: {e}") - - # Fallback to US/Central for Dallas - print("Using fallback timezone: US/Central") - return pytz.timezone('US/Central') + return pytz.timezone(config_timezone) + except pytz.exceptions.UnknownTimeZoneError: + logger.warning( + f"Invalid timezone '{config_timezone}' in config. " + "Falling back to UTC. Please check your config.json file. " + "A list of valid timezones can be found at " + "https://en.wikipedia.org/wiki/List_of_tz_database_time_zones" + ) + return pytz.utc def _get_ordinal_suffix(self, day: int) -> str: """Get the ordinal suffix for a day number (1st, 2nd, 3rd, etc.).""" diff --git a/src/nba_managers.py b/src/nba_managers.py index c457f4b7..e95d48a7 100644 --- a/src/nba_managers.py +++ b/src/nba_managers.py @@ -9,6 +9,8 @@ from pathlib import Path from datetime import datetime, timedelta, timezone from src.display_manager import DisplayManager from src.cache_manager import CacheManager +from src.config_manager import ConfigManager +import pytz # Constants ESPN_NBA_SCOREBOARD_URL = "https://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard" @@ -34,6 +36,7 @@ class BaseNBAManager: def __init__(self, config: Dict[str, Any], display_manager: DisplayManager): self.display_manager = display_manager + self.config_manager = ConfigManager() self.config = config self.nba_config = config.get("nba_scoreboard", {}) self.is_enabled = self.nba_config.get("enabled", False) @@ -380,14 +383,14 @@ class BaseNBAManager: game_date = "" if start_time_utc: # Convert to local time - local_time = start_time_utc.astimezone() + local_time = start_time_utc.astimezone(self._get_timezone()) game_time = local_time.strftime("%-I:%M %p") game_date = local_time.strftime("%-m/%-d") # Calculate if game is within recent window is_within_window = False if start_time_utc: - cutoff_time = datetime.now(timezone.utc) - timedelta(hours=self.recent_hours) + cutoff_time = datetime.now(self._get_timezone()) - timedelta(hours=self.recent_hours) is_within_window = start_time_utc > cutoff_time self.logger.debug(f"[NBA] Game time: {start_time_utc}, Cutoff time: {cutoff_time}, Within window: {is_within_window}") diff --git a/src/ncaa_fb_managers.py b/src/ncaa_fb_managers.py index 30c21ffd..0fa5a00a 100644 --- a/src/ncaa_fb_managers.py +++ b/src/ncaa_fb_managers.py @@ -9,6 +9,8 @@ from pathlib import Path from datetime import datetime, timedelta, timezone from src.display_manager import DisplayManager from src.cache_manager import CacheManager # Keep CacheManager import +from src.config_manager import ConfigManager +import pytz # Constants ESPN_NCAAFB_SCOREBOARD_URL = "https://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard" # Changed URL for NCAA FB @@ -85,6 +87,7 @@ class BaseNCAAFBManager: # Renamed class def __init__(self, config: Dict[str, Any], display_manager: DisplayManager): self.display_manager = display_manager + self.config_manager = ConfigManager() self.config = config self.ncaa_fb_config = config.get("ncaa_fb_scoreboard", {}) # Changed config key self.is_enabled = self.ncaa_fb_config.get("enabled", False) @@ -112,6 +115,12 @@ class BaseNCAAFBManager: # Renamed class self.logger.info(f"Initialized NCAAFB manager with display dimensions: {self.display_width}x{self.display_height}") self.logger.info(f"Logo directory: {self.logo_dir}") + def _get_timezone(self): + try: + return pytz.timezone(self.config_manager.get_timezone()) + except pytz.UnknownTimeZoneError: + return pytz.utc + @classmethod def _fetch_shared_data(cls, past_days: int, future_days: int, date_str: str = None) -> Optional[Dict]: """Fetch and cache data for all managers to share.""" @@ -144,7 +153,7 @@ class BaseNCAAFBManager: # Renamed class cls._last_shared_update = current_time if not date_str: - today = datetime.now(timezone.utc).date() + today = datetime.now(cls._get_timezone()).date() dates_to_fetch = [] # Generate dates from past_days ago to future_days ahead for i in range(-past_days, future_days + 1): @@ -303,7 +312,7 @@ class BaseNCAAFBManager: # Renamed class game_time, game_date = "", "" if start_time_utc: - local_time = start_time_utc.astimezone() + local_time = start_time_utc.astimezone(self._get_timezone()) game_time = local_time.strftime("%-I:%M %p") game_date = local_time.strftime("%-m/%-d") diff --git a/src/ncaam_basketball_managers.py b/src/ncaam_basketball_managers.py index cec348cb..5167f0a3 100644 --- a/src/ncaam_basketball_managers.py +++ b/src/ncaam_basketball_managers.py @@ -9,6 +9,8 @@ from pathlib import Path from datetime import datetime, timedelta, timezone from src.display_manager import DisplayManager from src.cache_manager import CacheManager +from src.config_manager import ConfigManager +import pytz # Constants ESPN_NCAAMB_SCOREBOARD_URL = "https://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard" @@ -34,6 +36,7 @@ class BaseNCAAMBasketballManager: def __init__(self, config: Dict[str, Any], display_manager: DisplayManager): self.display_manager = display_manager + self.config_manager = ConfigManager() self.config = config self.ncaam_basketball_config = config.get("ncaam_basketball_scoreboard", {}) self.is_enabled = self.ncaam_basketball_config.get("enabled", False) @@ -63,6 +66,12 @@ class BaseNCAAMBasketballManager: self.logger.info(f"Initialized NCAAMBasketball manager with display dimensions: {self.display_width}x{self.display_height}") self.logger.info(f"Logo directory: {self.logo_dir}") + def _get_timezone(self): + try: + return pytz.timezone(self.config_manager.get_timezone()) + except pytz.UnknownTimeZoneError: + return pytz.utc + def _should_log(self, message_type: str, cooldown: int = 300) -> bool: """Check if a message should be logged based on cooldown period.""" current_time = time.time() @@ -78,7 +87,7 @@ class BaseNCAAMBasketballManager: self.logger.info("[NCAAMBasketball] Loading test data") # Create test data with current time - now = datetime.now(timezone.utc) + now = datetime.now(self._get_timezone()) # Create test events for different scenarios events = [] @@ -308,7 +317,7 @@ class BaseNCAAMBasketballManager: # If no date specified, fetch data from multiple days if not date_str: # Get today's date in YYYYMMDD format - today = datetime.now(timezone.utc).date() + today = datetime.now(pytz.utc).date() dates_to_fetch = [ (today - timedelta(days=2)).strftime('%Y%m%d'), (today - timedelta(days=1)).strftime('%Y%m%d'), @@ -399,14 +408,14 @@ class BaseNCAAMBasketballManager: game_date = "" if start_time_utc: # Convert to local time - local_time = start_time_utc.astimezone() + local_time = start_time_utc.astimezone(self._get_timezone()) game_time = local_time.strftime("%-I:%M %p") game_date = local_time.strftime("%-m/%-d") # Calculate if game is within recent window is_within_window = False if start_time_utc: - cutoff_time = datetime.now(timezone.utc) - timedelta(hours=self.recent_hours) + cutoff_time = datetime.now(self._get_timezone()) - timedelta(hours=self.recent_hours) is_within_window = start_time_utc > cutoff_time self.logger.debug(f"[NCAAMBasketball] Game time: {start_time_utc}, Cutoff time: {cutoff_time}, Within window: {is_within_window}") diff --git a/src/nfl_managers.py b/src/nfl_managers.py index 4ee621d0..a4937663 100644 --- a/src/nfl_managers.py +++ b/src/nfl_managers.py @@ -8,10 +8,12 @@ from PIL import Image, ImageDraw, ImageFont from pathlib import Path from datetime import datetime, timedelta, timezone from src.display_manager import DisplayManager -from src.cache_manager import CacheManager # Keep CacheManager import +from src.cache_manager import CacheManager +from src.config_manager import ConfigManager +import pytz # Constants -ESPN_NFL_SCOREBOARD_URL = "https://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard" # Changed URL +ESPN_NFL_SCOREBOARD_URL = "https://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard" # Configure logging to match main configuration logging.basicConfig( @@ -85,6 +87,7 @@ class BaseNFLManager: # Renamed class def __init__(self, config: Dict[str, Any], display_manager: DisplayManager): self.display_manager = display_manager + self.config_manager = ConfigManager() self.config = config self.nfl_config = config.get("nfl_scoreboard", {}) # Changed config key self.is_enabled = self.nfl_config.get("enabled", False) @@ -112,6 +115,12 @@ class BaseNFLManager: # Renamed class self.logger.info(f"Initialized NFL manager with display dimensions: {self.display_width}x{self.display_height}") self.logger.info(f"Logo directory: {self.logo_dir}") + def _get_timezone(self): + try: + return pytz.timezone(self.config_manager.get_timezone()) + except pytz.UnknownTimeZoneError: + return pytz.utc + @classmethod def _fetch_shared_data(cls, past_days: int, future_days: int, date_str: str = None) -> Optional[Dict]: """Fetch and cache data for all managers to share.""" @@ -144,7 +153,7 @@ class BaseNFLManager: # Renamed class cls._last_shared_update = current_time if not date_str: - today = datetime.now(timezone.utc).date() + today = datetime.now(cls._get_timezone()).date() dates_to_fetch = [] # Generate dates from past_days ago to future_days ahead for i in range(-past_days, future_days + 1): @@ -303,7 +312,7 @@ class BaseNFLManager: # Renamed class game_time, game_date = "", "" if start_time_utc: - local_time = start_time_utc.astimezone() + local_time = start_time_utc.astimezone(self._get_timezone()) game_time = local_time.strftime("%-I:%M %p") game_date = local_time.strftime("%-m/%-d") @@ -557,7 +566,7 @@ class NFLLiveManager(BaseNFLManager): # Renamed class current_game_ids = {g['id'] for g in self.live_games} if new_game_ids != current_game_ids: - self.live_games = sorted(new_live_games, key=lambda g: g.get('start_time_utc') or datetime.now(timezone.utc)) # Sort by start time + self.live_games = sorted(new_live_games, key=lambda g: g.get('start_time_utc') or datetime.now(self._get_timezone())) # Sort by start time # Reset index if current game is gone or list is new if not self.current_game or self.current_game['id'] not in new_game_ids: self.current_game_index = 0 @@ -781,7 +790,7 @@ class NFLRecentManager(BaseNFLManager): # Renamed class team_games = processed_games # Show all recent games if no favorites defined # Sort by game time, most recent first - team_games.sort(key=lambda g: g.get('start_time_utc') or datetime.min.replace(tzinfo=timezone.utc), reverse=True) + team_games.sort(key=lambda g: g.get('start_time_utc') or datetime.min.replace(tzinfo=self._get_timezone()), reverse=True) # Check if the list of games to display has changed new_game_ids = {g['id'] for g in team_games} @@ -953,7 +962,7 @@ class NFLUpcomingManager(BaseNFLManager): # Renamed class team_games = processed_games # Show all upcoming if no favorites # Sort by game time, earliest first - team_games.sort(key=lambda g: g.get('start_time_utc') or datetime.max.replace(tzinfo=timezone.utc)) + team_games.sort(key=lambda g: g.get('start_time_utc') or datetime.max.replace(tzinfo=self._get_timezone())) # Log changes or periodically should_log = ( diff --git a/src/nhl_managers.py b/src/nhl_managers.py index b99b0969..e53a27b0 100644 --- a/src/nhl_managers.py +++ b/src/nhl_managers.py @@ -9,9 +9,11 @@ from pathlib import Path from datetime import datetime, timedelta, timezone from src.display_manager import DisplayManager from src.cache_manager import CacheManager +from src.config_manager import ConfigManager +import pytz # Constants -ESPN_NHL_SCOREBOARD_URL = "https://site.api.espn.com/apis/site/v2/sports/hockey/nhl/scoreboard" +NHL_API_BASE_URL = "https://api-web.nhle.com/v1/schedule/" # Configure logging to match main configuration logging.basicConfig( @@ -83,6 +85,7 @@ class BaseNHLManager: def __init__(self, config: Dict[str, Any], display_manager: DisplayManager): self.display_manager = display_manager + self.config_manager = ConfigManager() self.config = config self.nhl_config = config.get("nhl_scoreboard", {}) self.is_enabled = self.nhl_config.get("enabled", False) @@ -112,6 +115,12 @@ class BaseNHLManager: self.logger.info(f"Initialized NHL manager with display dimensions: {self.display_width}x{self.display_height}") self.logger.info(f"Logo directory: {self.logo_dir}") + def _get_timezone(self): + try: + return pytz.timezone(self.config_manager.get_timezone()) + except pytz.UnknownTimeZoneError: + return pytz.utc + @classmethod def _fetch_shared_data(cls, date_str: str = None) -> Optional[Dict]: """Fetch and cache data for all managers to share.""" @@ -132,15 +141,18 @@ class BaseNHLManager: return cached_data # If not in cache or stale, fetch from API - url = ESPN_NHL_SCOREBOARD_URL - params = {} - if date_str: - params['dates'] = date_str + if not date_str: + # Get today's date in YYYY-MM-DD format + today = datetime.now(cls._get_timezone()).date() + date_str = today.strftime('%Y-%m-%d') - response = requests.get(url, params=params) + url = f"{NHL_API_BASE_URL}{date_str}" + cls.logger.info(f"Fetching data from URL: {url}") + + response = requests.get(url) response.raise_for_status() data = response.json() - cls.logger.info(f"[NHL] Successfully fetched data from ESPN API") + cls.logger.info(f"[NHL] Successfully fetched data from NHL API") # Cache the response cls.cache_manager.set(cache_key, data) @@ -150,17 +162,17 @@ class BaseNHLManager: # If no date specified, fetch data from multiple days if not date_str: # Get today's date in YYYYMMDD format - today = datetime.now(timezone.utc).date() + today = datetime.now(cls._get_timezone()).date() dates_to_fetch = [ - (today - timedelta(days=2)).strftime('%Y%m%d'), - (today - timedelta(days=1)).strftime('%Y%m%d'), - today.strftime('%Y%m%d') + (today - timedelta(days=2)).strftime('%Y-%m-%d'), + (today - timedelta(days=1)).strftime('%Y-%m-%d'), + today.strftime('%Y-%m-%d') ] # Fetch data for each date all_events = [] for fetch_date in dates_to_fetch: - if fetch_date != today.strftime('%Y%m%d'): # Skip today as we already have it + if fetch_date != today.strftime('%Y-%m-%d'): # Skip today as we already have it # Check cache for this date cached_date_data = cls.cache_manager.get(fetch_date, max_age=300) if cached_date_data: @@ -169,8 +181,8 @@ class BaseNHLManager: all_events.extend(cached_date_data["events"]) continue - params['dates'] = fetch_date - response = requests.get(url, params=params) + url = f"{NHL_API_BASE_URL}{fetch_date}" + response = requests.get(url) response.raise_for_status() date_data = response.json() if date_data and "events" in date_data: @@ -188,7 +200,7 @@ class BaseNHLManager: return data except requests.exceptions.RequestException as e: - cls.logger.error(f"[NHL] Error fetching data from ESPN: {e}") + cls.logger.error(f"[NHL] Error fetching data from NHL: {e}") return None def _fetch_data(self, date_str: str = None) -> Optional[Dict]: @@ -196,18 +208,14 @@ class BaseNHLManager: # For live games, bypass the shared cache to ensure fresh data if isinstance(self, NHLLiveManager): try: - url = ESPN_NHL_SCOREBOARD_URL - params = {} - if date_str: - params['dates'] = date_str - - response = requests.get(url, params=params) + url = f"{NHL_API_BASE_URL}{date_str}" if date_str else f"{NHL_API_BASE_URL}{datetime.now(self._get_timezone()).strftime('%Y-%m-%d')}" + response = requests.get(url) response.raise_for_status() data = response.json() - self.logger.info(f"[NHL] Successfully fetched live game data from ESPN API") + self.logger.info(f"[NHL] Successfully fetched live game data from NHL API") return data except requests.exceptions.RequestException as e: - self.logger.error(f"[NHL] Error fetching live game data from ESPN: {e}") + self.logger.error(f"[NHL] Error fetching live game data from NHL: {e}") return None else: # For non-live games, use the shared cache @@ -344,7 +352,7 @@ class BaseNHLManager: game_date = "" if start_time_utc: # Convert to local time - local_time = start_time_utc.astimezone() + local_time = start_time_utc.astimezone(self._get_timezone()) game_time = local_time.strftime("%-I:%M %p") game_date = local_time.strftime("%-m/%-d") @@ -353,13 +361,13 @@ class BaseNHLManager: if start_time_utc: # For upcoming games, check if the game is within the next 48 hours if status["type"]["state"] == "pre": - cutoff_time = datetime.now(timezone.utc) + timedelta(hours=self.recent_hours) + cutoff_time = datetime.now(pytz.utc) + timedelta(hours=self.recent_hours) is_within_window = start_time_utc <= cutoff_time self.logger.info(f"[NHL] Game time: {start_time_utc}, Cutoff time: {cutoff_time}, Within window: {is_within_window}") self.logger.info(f"[NHL] Game status: {status['type']['state']}, Home: {home_team['team']['abbreviation']}, Away: {away_team['team']['abbreviation']}") else: # For recent games, check if the game is within the last 48 hours - cutoff_time = datetime.now(timezone.utc) - timedelta(hours=self.recent_hours) + cutoff_time = datetime.now(pytz.utc) - timedelta(hours=self.recent_hours) is_within_window = start_time_utc > cutoff_time self.logger.debug(f"[NHL] Game time: {start_time_utc}, Cutoff time: {cutoff_time}, Within window: {is_within_window}") diff --git a/src/soccer_managers.py b/src/soccer_managers.py index 394b48b0..e2aba317 100644 --- a/src/soccer_managers.py +++ b/src/soccer_managers.py @@ -10,6 +10,8 @@ from pathlib import Path from datetime import datetime, timedelta, timezone from src.display_manager import DisplayManager from src.cache_manager import CacheManager +from src.config_manager import ConfigManager +import pytz # Constants # ESPN_SOCCER_SCOREBOARD_URL = "https://site.api.espn.com/apis/site/v2/sports/soccer/scoreboards" # Old URL @@ -143,6 +145,8 @@ class BaseSoccerManager: self.logger.info(f"Team map file: {self.team_map_file}") self.logger.info(f"Team map update interval: {self.team_map_update_days} days") + self.config_manager = ConfigManager(config) + # --- Team League Map Management --- @classmethod def _load_team_league_map(cls) -> None: @@ -182,7 +186,7 @@ class BaseSoccerManager: """Fetch data for all known leagues to build the team-to-league map.""" cls.logger.info("[Soccer] Building team-league map...") new_map = {} - yesterday = (datetime.now(timezone.utc) - timedelta(days=1)).strftime('%Y%m%d') + yesterday = (datetime.now(pytz.utc) - timedelta(days=1)).strftime('%Y%m%d') # Fetch data for all leagues defined in LEAGUE_SLUGS to get comprehensive team info for league_slug in LEAGUE_SLUGS.keys(): @@ -265,7 +269,7 @@ class BaseSoccerManager: cls.logger.debug(f"[Soccer] Determined leagues to fetch for shared data: {leagues_to_fetch}") - today = datetime.now(timezone.utc).date() + today = datetime.now(pytz.utc).date() # Generate dates from yesterday up to 'upcoming_fetch_days' in the future dates_to_fetch = [ (today + timedelta(days=i)).strftime('%Y%m%d') @@ -366,7 +370,7 @@ class BaseSoccerManager: if isinstance(self, SoccerLiveManager) and not self.test_mode: # Live manager bypasses shared cache; fetches today's data per league live_data = {"events": []} - today_date_str = datetime.now(timezone.utc).strftime('%Y%m%d') + today_date_str = datetime.now(pytz.utc).strftime('%Y%m%d') # Determine leagues to fetch based on favorites and map leagues_to_fetch = self._get_live_leagues_to_fetch() @@ -533,7 +537,7 @@ class BaseSoccerManager: game_time = "" game_date = "" if start_time_utc: - local_time = start_time_utc.astimezone() + local_time = start_time_utc.astimezone(self._get_timezone()) game_time = local_time.strftime("%-I:%M%p").lower() # e.g., 2:30pm game_date = local_time.strftime("%-m/%-d") @@ -546,7 +550,7 @@ class BaseSoccerManager: # Calculate if game is within recent/upcoming window is_within_window = False if start_time_utc: - now_utc = datetime.now(timezone.utc) + now_utc = datetime.now(pytz.utc) if is_upcoming: cutoff_time = now_utc + timedelta(hours=self.recent_hours) is_within_window = start_time_utc <= cutoff_time @@ -804,7 +808,7 @@ class SoccerLiveManager(BaseSoccerManager): current_game_ids = {game['id'] for game in self.live_games} if new_game_ids != current_game_ids: - self.live_games = sorted(new_live_games, key=lambda x: x['start_time_utc'] or datetime.now(timezone.utc)) # Sort by time + self.live_games = sorted(new_live_games, key=lambda x: x['start_time_utc'] or datetime.now(pytz.utc)) # Sort by time # Reset index if current game is gone or list is new if not self.current_game or self.current_game['id'] not in new_game_ids: self.current_game_index = 0 @@ -902,7 +906,7 @@ class SoccerRecentManager(BaseSoccerManager): # Process and filter games new_recent_games = [] - now_utc = datetime.now(timezone.utc) + now_utc = datetime.now(pytz.utc) cutoff_time = now_utc - timedelta(hours=self.recent_hours) for event in data['events']: @@ -1002,7 +1006,7 @@ class SoccerUpcomingManager(BaseSoccerManager): # Process and filter games new_upcoming_games = [] - now_utc = datetime.now(timezone.utc) + now_utc = datetime.now(pytz.utc) cutoff_time = now_utc + timedelta(hours=self.recent_hours) # Use recent_hours as upcoming window for event in data['events']: