Merge missing PRs from main: NCAA Hockey (#36), Emulator Support (#35), NCAA FB AP rankings (#17), NCAA FB logos (#15)
- Added NCAA Hockey support with new manager and logos - Added emulator support with requirements file - Added NCAA FB AP top 25 rankings functionality - Added NCAA FB logo download capability - Resolved conflicts by keeping development branch improvements while adding missing features
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- OS: [e.g. iOS]
|
||||||
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Smartphone (please complete the following information):**
|
||||||
|
- Device: [e.g. iPhone6]
|
||||||
|
- OS: [e.g. iOS8.1]
|
||||||
|
- Browser [e.g. stock browser, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
BIN
assets/sports/ncaa_logos/AIC.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
assets/sports/ncaa_logos/BU.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
assets/sports/ncaa_logos/DAL.png
Normal file
|
After Width: | Height: | Size: 386 B |
BIN
assets/sports/ncaa_logos/DEN.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
assets/sports/ncaa_logos/ME.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
assets/sports/ncaa_logos/MSU.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
assets/sports/ncaa_logos/PU.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
assets/sports/ncaa_logos/RIT.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
assets/sports/ncaa_logos/SHU.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
assets/sports/ncaa_logos/TB.png
Normal file
|
After Width: | Height: | Size: 341 B |
BIN
assets/sports/ncaa_logos/UIW.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
assets/sports/ncaa_logos/UTSA.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
assets/sports/ncaa_logos/ncaah.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
@@ -180,6 +180,11 @@
|
|||||||
"ncaam_basketball": {
|
"ncaam_basketball": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"top_teams": 25
|
"top_teams": 25
|
||||||
|
},
|
||||||
|
"ncaam_hockey": {
|
||||||
|
"enabled": true,
|
||||||
|
"top_teams": 10,
|
||||||
|
"show_ranking": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"update_interval": 3600,
|
"update_interval": 3600,
|
||||||
@@ -354,6 +359,32 @@
|
|||||||
"ncaam_basketball_upcoming": true
|
"ncaam_basketball_upcoming": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ncaam_hockey_scoreboard": {
|
||||||
|
"enabled": true,
|
||||||
|
"live_priority": true,
|
||||||
|
"live_game_duration": 20,
|
||||||
|
"show_odds": true,
|
||||||
|
"test_mode": false,
|
||||||
|
"update_interval_seconds": 3600,
|
||||||
|
"live_update_interval": 30,
|
||||||
|
"live_odds_update_interval": 3600,
|
||||||
|
"odds_update_interval": 3600,
|
||||||
|
"season_cache_duration_seconds": 86400,
|
||||||
|
"recent_games_to_show": 1,
|
||||||
|
"upcoming_games_to_show": 1,
|
||||||
|
"show_favorite_teams_only": true,
|
||||||
|
"favorite_teams": [
|
||||||
|
"RIT"
|
||||||
|
],
|
||||||
|
"logo_dir": "assets/sports/ncaa_logos",
|
||||||
|
"show_records": true,
|
||||||
|
"show_ranking": true,
|
||||||
|
"display_modes": {
|
||||||
|
"ncaam_hockey_live": true,
|
||||||
|
"ncaam_hockey_recent": true ,
|
||||||
|
"ncaam_hockey_upcoming": true
|
||||||
|
}
|
||||||
|
},
|
||||||
"youtube": {
|
"youtube": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"update_interval": 3600
|
"update_interval": 3600
|
||||||
|
|||||||
1
requirements-emulator.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
RGBMatrixEmulator
|
||||||
@@ -9,7 +9,6 @@ from googleapiclient.discovery import build
|
|||||||
import pickle
|
import pickle
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from rgbmatrix import graphics
|
|
||||||
import pytz
|
import pytz
|
||||||
from src.config_manager import ConfigManager
|
from src.config_manager import ConfigManager
|
||||||
import time
|
import time
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from src.nfl_managers import NFLLiveManager, NFLRecentManager, NFLUpcomingManage
|
|||||||
from src.ncaa_fb_managers import NCAAFBLiveManager, NCAAFBRecentManager, NCAAFBUpcomingManager
|
from src.ncaa_fb_managers import NCAAFBLiveManager, NCAAFBRecentManager, NCAAFBUpcomingManager
|
||||||
from src.ncaa_baseball_managers import NCAABaseballLiveManager, NCAABaseballRecentManager, NCAABaseballUpcomingManager
|
from src.ncaa_baseball_managers import NCAABaseballLiveManager, NCAABaseballRecentManager, NCAABaseballUpcomingManager
|
||||||
from src.ncaam_basketball_managers import NCAAMBasketballLiveManager, NCAAMBasketballRecentManager, NCAAMBasketballUpcomingManager
|
from src.ncaam_basketball_managers import NCAAMBasketballLiveManager, NCAAMBasketballRecentManager, NCAAMBasketballUpcomingManager
|
||||||
|
from src.ncaam_hockey_managers import NCAAMHockeyLiveManager, NCAAMHockeyRecentManager, NCAAMHockeyUpcomingManager
|
||||||
from src.youtube_display import YouTubeDisplay
|
from src.youtube_display import YouTubeDisplay
|
||||||
from src.calendar_manager import CalendarManager
|
from src.calendar_manager import CalendarManager
|
||||||
from src.text_display import TextDisplay
|
from src.text_display import TextDisplay
|
||||||
@@ -236,6 +237,21 @@ class DisplayController:
|
|||||||
self.ncaam_basketball_upcoming = None
|
self.ncaam_basketball_upcoming = None
|
||||||
logger.info("NCAA Men's Basketball managers initialized in %.3f seconds", time.time() - ncaam_basketball_time)
|
logger.info("NCAA Men's Basketball managers initialized in %.3f seconds", time.time() - ncaam_basketball_time)
|
||||||
|
|
||||||
|
# Initialize NCAA Men's Hockey managers if enabled
|
||||||
|
ncaam_hockey_time = time.time()
|
||||||
|
ncaam_hockey_enabled = self.config.get('ncaam_hockey_scoreboard', {}).get('enabled', False)
|
||||||
|
ncaam_hockey_display_modes = self.config.get('ncaam_hockey_scoreboard', {}).get('display_modes', {})
|
||||||
|
|
||||||
|
if ncaam_hockey_enabled:
|
||||||
|
self.ncaam_hockey_live = NCAAMHockeyLiveManager(self.config, self.display_manager, self.cache_manager) if ncaam_hockey_display_modes.get('ncaam_hockey_live', True) else None
|
||||||
|
self.ncaam_hockey_recent = NCAAMHockeyRecentManager(self.config, self.display_manager, self.cache_manager) if ncaam_hockey_display_modes.get('ncaam_hockey_recent', True) else None
|
||||||
|
self.ncaam_hockey_upcoming = NCAAMHockeyUpcomingManager(self.config, self.display_manager, self.cache_manager) if ncaam_hockey_display_modes.get('ncaam_hockey_upcoming', True) else None
|
||||||
|
else:
|
||||||
|
self.ncaam_hockey_live = None
|
||||||
|
self.ncaam_hockey_recent = None
|
||||||
|
self.ncaam_hockey_upcoming = None
|
||||||
|
logger.info("NCAA Men's Hockey managers initialized in %.3f seconds", time.time() - ncaam_hockey_time)
|
||||||
|
|
||||||
# Track MLB rotation state
|
# Track MLB rotation state
|
||||||
self.mlb_current_team_index = 0
|
self.mlb_current_team_index = 0
|
||||||
self.mlb_showing_recent = True
|
self.mlb_showing_recent = True
|
||||||
@@ -252,6 +268,7 @@ class DisplayController:
|
|||||||
self.ncaa_fb_live_priority = self.config.get('ncaa_fb_scoreboard', {}).get('live_priority', True)
|
self.ncaa_fb_live_priority = self.config.get('ncaa_fb_scoreboard', {}).get('live_priority', True)
|
||||||
self.ncaa_baseball_live_priority = self.config.get('ncaa_baseball_scoreboard', {}).get('live_priority', True)
|
self.ncaa_baseball_live_priority = self.config.get('ncaa_baseball_scoreboard', {}).get('live_priority', True)
|
||||||
self.ncaam_basketball_live_priority = self.config.get('ncaam_basketball_scoreboard', {}).get('live_priority', True)
|
self.ncaam_basketball_live_priority = self.config.get('ncaam_basketball_scoreboard', {}).get('live_priority', True)
|
||||||
|
self.ncaam_hockey_live_priority = self.config.get('ncaam_hockey_scoreboard', {}).get('live_priority', True)
|
||||||
|
|
||||||
# List of available display modes (adjust order as desired)
|
# List of available display modes (adjust order as desired)
|
||||||
self.available_modes = []
|
self.available_modes = []
|
||||||
@@ -297,6 +314,9 @@ class DisplayController:
|
|||||||
if ncaam_basketball_enabled:
|
if ncaam_basketball_enabled:
|
||||||
if self.ncaam_basketball_recent: self.available_modes.append('ncaam_basketball_recent')
|
if self.ncaam_basketball_recent: self.available_modes.append('ncaam_basketball_recent')
|
||||||
if self.ncaam_basketball_upcoming: self.available_modes.append('ncaam_basketball_upcoming')
|
if self.ncaam_basketball_upcoming: self.available_modes.append('ncaam_basketball_upcoming')
|
||||||
|
if ncaam_hockey_enabled:
|
||||||
|
if self.ncaam_hockey_recent: self.available_modes.append('ncaam_hockey_recent')
|
||||||
|
if self.ncaam_hockey_upcoming: self.available_modes.append('ncaam_hockey_upcoming')
|
||||||
# Add live modes to rotation if live_priority is False and there are live games
|
# Add live modes to rotation if live_priority is False and there are live games
|
||||||
self._update_live_modes_in_rotation()
|
self._update_live_modes_in_rotation()
|
||||||
|
|
||||||
@@ -399,7 +419,10 @@ class DisplayController:
|
|||||||
'ncaa_baseball_upcoming': 15,
|
'ncaa_baseball_upcoming': 15,
|
||||||
'ncaam_basketball_live': 30, # Added NCAA Men's Basketball durations
|
'ncaam_basketball_live': 30, # Added NCAA Men's Basketball durations
|
||||||
'ncaam_basketball_recent': 15,
|
'ncaam_basketball_recent': 15,
|
||||||
'ncaam_basketball_upcoming': 15
|
'ncaam_basketball_upcoming': 15,
|
||||||
|
'ncaam_hockey_live': 30, # Added NCAA Men's Hockey durations
|
||||||
|
'ncaam_hockey_recent': 15,
|
||||||
|
'ncaam_hockey_upcoming': 15
|
||||||
}
|
}
|
||||||
# Merge loaded durations with defaults
|
# Merge loaded durations with defaults
|
||||||
for key, value in default_durations.items():
|
for key, value in default_durations.items():
|
||||||
@@ -627,6 +650,10 @@ class DisplayController:
|
|||||||
if self.ncaam_basketball_live: self.ncaam_basketball_live.update()
|
if self.ncaam_basketball_live: self.ncaam_basketball_live.update()
|
||||||
if self.ncaam_basketball_recent: self.ncaam_basketball_recent.update()
|
if self.ncaam_basketball_recent: self.ncaam_basketball_recent.update()
|
||||||
if self.ncaam_basketball_upcoming: self.ncaam_basketball_upcoming.update()
|
if self.ncaam_basketball_upcoming: self.ncaam_basketball_upcoming.update()
|
||||||
|
elif current_sport == 'ncaam_hockey':
|
||||||
|
if self.ncaam_hockey_live: self.ncaam_hockey_live.update()
|
||||||
|
if self.ncaam_hockey_recent: self.ncaam_hockey_recent.update()
|
||||||
|
if self.ncaam_hockey_upcoming: self.ncaam_hockey_upcoming.update()
|
||||||
else:
|
else:
|
||||||
# If no specific sport is active, update all managers (fallback behavior)
|
# If no specific sport is active, update all managers (fallback behavior)
|
||||||
# This ensures data is available when switching to a sport
|
# This ensures data is available when switching to a sport
|
||||||
@@ -666,6 +693,10 @@ class DisplayController:
|
|||||||
if self.ncaam_basketball_recent: self.ncaam_basketball_recent.update()
|
if self.ncaam_basketball_recent: self.ncaam_basketball_recent.update()
|
||||||
if self.ncaam_basketball_upcoming: self.ncaam_basketball_upcoming.update()
|
if self.ncaam_basketball_upcoming: self.ncaam_basketball_upcoming.update()
|
||||||
|
|
||||||
|
if self.ncaam_hockey_live: self.ncaam_hockey_live.update()
|
||||||
|
if self.ncaam_hockey_recent: self.ncaam_hockey_recent.update()
|
||||||
|
if self.ncaam_hockey_upcoming: self.ncaam_hockey_upcoming.update()
|
||||||
|
|
||||||
def _check_live_games(self) -> tuple:
|
def _check_live_games(self) -> tuple:
|
||||||
"""
|
"""
|
||||||
Check if there are any live games available.
|
Check if there are any live games available.
|
||||||
@@ -693,6 +724,8 @@ class DisplayController:
|
|||||||
live_checks['ncaa_baseball'] = self.ncaa_baseball_live and self.ncaa_baseball_live.live_games
|
live_checks['ncaa_baseball'] = self.ncaa_baseball_live and self.ncaa_baseball_live.live_games
|
||||||
if 'ncaam_basketball_scoreboard' in self.config and self.config['ncaam_basketball_scoreboard'].get('enabled', False):
|
if 'ncaam_basketball_scoreboard' in self.config and self.config['ncaam_basketball_scoreboard'].get('enabled', False):
|
||||||
live_checks['ncaam_basketball'] = self.ncaam_basketball_live and self.ncaam_basketball_live.live_games
|
live_checks['ncaam_basketball'] = self.ncaam_basketball_live and self.ncaam_basketball_live.live_games
|
||||||
|
if 'ncaam_hockey_scoreboard' in self.config and self.config['ncaam_hockey_scoreboard'].get('enabled', False):
|
||||||
|
live_checks['ncaam_hockey'] = self.ncaam_hockey_live and self.ncaam_hockey_live.live_games
|
||||||
|
|
||||||
for sport, has_live_games in live_checks.items():
|
for sport, has_live_games in live_checks.items():
|
||||||
if has_live_games:
|
if has_live_games:
|
||||||
@@ -943,6 +976,7 @@ class DisplayController:
|
|||||||
ncaa_fb_enabled = self.config.get('ncaa_fb_scoreboard', {}).get('enabled', False)
|
ncaa_fb_enabled = self.config.get('ncaa_fb_scoreboard', {}).get('enabled', False)
|
||||||
ncaa_baseball_enabled = self.config.get('ncaa_baseball_scoreboard', {}).get('enabled', False)
|
ncaa_baseball_enabled = self.config.get('ncaa_baseball_scoreboard', {}).get('enabled', False)
|
||||||
ncaam_basketball_enabled = self.config.get('ncaam_basketball_scoreboard', {}).get('enabled', False)
|
ncaam_basketball_enabled = self.config.get('ncaam_basketball_scoreboard', {}).get('enabled', False)
|
||||||
|
ncaam_hockey_enabled = self.config.get('ncaam_hockey_scoreboard', {}).get('enabled', False)
|
||||||
|
|
||||||
update_mode('nhl_live', getattr(self, 'nhl_live', None), self.nhl_live_priority, nhl_enabled)
|
update_mode('nhl_live', getattr(self, 'nhl_live', None), self.nhl_live_priority, nhl_enabled)
|
||||||
update_mode('nba_live', getattr(self, 'nba_live', None), self.nba_live_priority, nba_enabled)
|
update_mode('nba_live', getattr(self, 'nba_live', None), self.nba_live_priority, nba_enabled)
|
||||||
@@ -953,6 +987,7 @@ class DisplayController:
|
|||||||
update_mode('ncaa_fb_live', getattr(self, 'ncaa_fb_live', None), self.ncaa_fb_live_priority, ncaa_fb_enabled)
|
update_mode('ncaa_fb_live', getattr(self, 'ncaa_fb_live', None), self.ncaa_fb_live_priority, ncaa_fb_enabled)
|
||||||
update_mode('ncaa_baseball_live', getattr(self, 'ncaa_baseball_live', None), self.ncaa_baseball_live_priority, ncaa_baseball_enabled)
|
update_mode('ncaa_baseball_live', getattr(self, 'ncaa_baseball_live', None), self.ncaa_baseball_live_priority, ncaa_baseball_enabled)
|
||||||
update_mode('ncaam_basketball_live', getattr(self, 'ncaam_basketball_live', None), self.ncaam_basketball_live_priority, ncaam_basketball_enabled)
|
update_mode('ncaam_basketball_live', getattr(self, 'ncaam_basketball_live', None), self.ncaam_basketball_live_priority, ncaam_basketball_enabled)
|
||||||
|
update_mode('ncaam_hockey_live', getattr(self, 'ncaam_hockey_live', None), self.ncaam_hockey_live_priority, ncaam_hockey_enabled)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Run the display controller, switching between displays."""
|
"""Run the display controller, switching between displays."""
|
||||||
@@ -995,7 +1030,8 @@ class DisplayController:
|
|||||||
('nfl', 'nfl_live', self.nfl_live_priority),
|
('nfl', 'nfl_live', self.nfl_live_priority),
|
||||||
('ncaa_fb', 'ncaa_fb_live', self.ncaa_fb_live_priority),
|
('ncaa_fb', 'ncaa_fb_live', self.ncaa_fb_live_priority),
|
||||||
('ncaa_baseball', 'ncaa_baseball_live', self.ncaa_baseball_live_priority),
|
('ncaa_baseball', 'ncaa_baseball_live', self.ncaa_baseball_live_priority),
|
||||||
('ncaam_basketball', 'ncaam_basketball_live', self.ncaam_basketball_live_priority)
|
('ncaam_basketball', 'ncaam_basketball_live', self.ncaam_basketball_live_priority),
|
||||||
|
('ncaam_hockey', 'ncaam_hockey_live', self.ncaam_hockey_live_priority)
|
||||||
]:
|
]:
|
||||||
manager = getattr(self, attr, None)
|
manager = getattr(self, attr, None)
|
||||||
# Only consider sports that are enabled (manager is not None) and have actual live games
|
# Only consider sports that are enabled (manager is not None) and have actual live games
|
||||||
@@ -1196,6 +1232,12 @@ class DisplayController:
|
|||||||
manager_to_display = self.ncaa_baseball_live
|
manager_to_display = self.ncaa_baseball_live
|
||||||
elif self.current_display_mode == 'ncaam_basketball_live' and self.ncaam_basketball_live:
|
elif self.current_display_mode == 'ncaam_basketball_live' and self.ncaam_basketball_live:
|
||||||
manager_to_display = self.ncaam_basketball_live
|
manager_to_display = self.ncaam_basketball_live
|
||||||
|
elif self.current_display_mode == 'ncaam_hockey_live' and self.ncaam_hockey_live:
|
||||||
|
manager_to_display = self.ncaam_hockey_live
|
||||||
|
elif self.current_display_mode == 'ncaam_hockey_recent' and self.ncaam_hockey_recent:
|
||||||
|
manager_to_display = self.ncaam_hockey_recent
|
||||||
|
elif self.current_display_mode == 'ncaam_hockey_upcoming' and self.ncaam_hockey_upcoming:
|
||||||
|
manager_to_display = self.ncaam_hockey_upcoming
|
||||||
elif self.current_display_mode == 'mlb_live' and self.mlb_live:
|
elif self.current_display_mode == 'mlb_live' and self.mlb_live:
|
||||||
manager_to_display = self.mlb_live
|
manager_to_display = self.mlb_live
|
||||||
elif self.current_display_mode == 'milb_live' and self.milb_live:
|
elif self.current_display_mode == 'milb_live' and self.milb_live:
|
||||||
@@ -1260,6 +1302,10 @@ class DisplayController:
|
|||||||
self.ncaa_baseball_recent.display(force_clear=self.force_clear)
|
self.ncaa_baseball_recent.display(force_clear=self.force_clear)
|
||||||
elif self.current_display_mode == 'ncaa_baseball_upcoming' and self.ncaa_baseball_upcoming:
|
elif self.current_display_mode == 'ncaa_baseball_upcoming' and self.ncaa_baseball_upcoming:
|
||||||
self.ncaa_baseball_upcoming.display(force_clear=self.force_clear)
|
self.ncaa_baseball_upcoming.display(force_clear=self.force_clear)
|
||||||
|
elif self.current_display_mode == 'ncaam_hockey_recent' and self.ncaam_hockey_recent:
|
||||||
|
self.ncaam_hockey_recent.display(force_clear=self.force_clear)
|
||||||
|
elif self.current_display_mode == 'ncaam_hockey_upcoming' and self.ncaam_hockey_upcoming:
|
||||||
|
self.ncaam_hockey_upcoming.display(force_clear=self.force_clear)
|
||||||
elif self.current_display_mode == 'milb_live' and self.milb_live and len(self.milb_live.live_games) > 0:
|
elif self.current_display_mode == 'milb_live' and self.milb_live and len(self.milb_live.live_games) > 0:
|
||||||
logger.debug(f"[DisplayController] Calling MiLB live display with {len(self.milb_live.live_games)} live games")
|
logger.debug(f"[DisplayController] Calling MiLB live display with {len(self.milb_live.live_games)} live games")
|
||||||
# Update data before displaying for live managers
|
# Update data before displaying for live managers
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
from rgbmatrix import RGBMatrix, RGBMatrixOptions
|
import os
|
||||||
|
if os.getenv("EMULATOR", "false") == "true":
|
||||||
|
from RGBMatrixEmulator import RGBMatrix, RGBMatrixOptions
|
||||||
|
else:
|
||||||
|
from rgbmatrix import RGBMatrix, RGBMatrixOptions
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
import time
|
import time
|
||||||
from typing import Dict, Any, List, Tuple
|
from typing import Dict, Any, List, Tuple
|
||||||
|
|||||||
@@ -1,22 +1,17 @@
|
|||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
import requests
|
import requests
|
||||||
import json
|
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
from datetime import datetime, timedelta, timezone
|
|
||||||
import os
|
import os
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
import pytz
|
|
||||||
try:
|
try:
|
||||||
from .display_manager import DisplayManager
|
from .display_manager import DisplayManager
|
||||||
from .cache_manager import CacheManager
|
from .cache_manager import CacheManager
|
||||||
from .config_manager import ConfigManager
|
|
||||||
from .logo_downloader import download_missing_logo
|
from .logo_downloader import download_missing_logo
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Fallback for direct imports
|
# Fallback for direct imports
|
||||||
from display_manager import DisplayManager
|
from display_manager import DisplayManager
|
||||||
from cache_manager import CacheManager
|
from cache_manager import CacheManager
|
||||||
from config_manager import ConfigManager
|
|
||||||
from logo_downloader import download_missing_logo
|
from logo_downloader import download_missing_logo
|
||||||
|
|
||||||
# Import the API counter function from web interface
|
# Import the API counter function from web interface
|
||||||
@@ -149,7 +144,16 @@ class LeaderboardManager:
|
|||||||
'season': self.enabled_sports.get('ncaa_baseball', {}).get('season', 2025),
|
'season': self.enabled_sports.get('ncaa_baseball', {}).get('season', 2025),
|
||||||
'level': self.enabled_sports.get('ncaa_baseball', {}).get('level', 1),
|
'level': self.enabled_sports.get('ncaa_baseball', {}).get('level', 1),
|
||||||
'sort': self.enabled_sports.get('ncaa_baseball', {}).get('sort', 'winpercent:desc,gamesbehind:asc')
|
'sort': self.enabled_sports.get('ncaa_baseball', {}).get('sort', 'winpercent:desc,gamesbehind:asc')
|
||||||
}
|
},
|
||||||
|
'ncaam_hockey': {
|
||||||
|
'sport': 'hockey',
|
||||||
|
'league': 'mens-college-hockey',
|
||||||
|
'logo_dir': 'assets/sports/ncaa_logos',
|
||||||
|
'league_logo': 'assets/sports/ncaa_logos/ncaah.png',
|
||||||
|
'teams_url': 'https://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-hockey/teams',
|
||||||
|
'enabled': self.enabled_sports.get('ncaam_hockey', {}).get('enabled', False),
|
||||||
|
'top_teams': self.enabled_sports.get('ncaam_hockey', {}).get('top_teams', 25)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(f"LeaderboardManager initialized with enabled sports: {[k for k, v in self.league_configs.items() if v['enabled']]}")
|
logger.info(f"LeaderboardManager initialized with enabled sports: {[k for k, v in self.league_configs.items() if v['enabled']]}")
|
||||||
@@ -290,6 +294,9 @@ class LeaderboardManager:
|
|||||||
if league_key == 'college-football':
|
if league_key == 'college-football':
|
||||||
return self._fetch_ncaa_fb_rankings(league_config)
|
return self._fetch_ncaa_fb_rankings(league_config)
|
||||||
|
|
||||||
|
if league_key == 'mens-college-hockey':
|
||||||
|
return self._fetch_ncaam_hockey_rankings(league_config)
|
||||||
|
|
||||||
# Use standings endpoint for NFL, MLB, NHL, and NCAA Baseball
|
# Use standings endpoint for NFL, MLB, NHL, and NCAA Baseball
|
||||||
if league_key in ['nfl', 'mlb', 'nhl', 'college-baseball']:
|
if league_key in ['nfl', 'mlb', 'nhl', 'college-baseball']:
|
||||||
return self._fetch_standings_data(league_config)
|
return self._fetch_standings_data(league_config)
|
||||||
@@ -472,6 +479,111 @@ class LeaderboardManager:
|
|||||||
logger.error(f"Error fetching rankings for {league_key}: {e}")
|
logger.error(f"Error fetching rankings for {league_key}: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def _fetch_ncaam_hockey_rankings(self, league_config: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
|
"""Fetch NCAA Hockey rankings from ESPN API using the rankings endpoint."""
|
||||||
|
league_key = league_config['league']
|
||||||
|
cache_key = f"leaderboard_{league_key}_rankings"
|
||||||
|
|
||||||
|
# Try to get cached data first
|
||||||
|
cached_data = self.cache_manager.get_cached_data_with_strategy(cache_key, 'leaderboard')
|
||||||
|
if cached_data:
|
||||||
|
logger.info(f"Using cached rankings data for {league_key}")
|
||||||
|
return cached_data.get('standings', [])
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(f"Fetching fresh rankings data for {league_key}")
|
||||||
|
rankings_url = "https://site.api.espn.com/apis/site/v2/sports/hockey/mens-college-hockey/rankings"
|
||||||
|
|
||||||
|
# Get rankings data
|
||||||
|
response = requests.get(rankings_url, timeout=self.request_timeout)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
# Increment API counter for sports data
|
||||||
|
increment_api_counter('sports', 1)
|
||||||
|
|
||||||
|
logger.info(f"Available rankings: {[rank['name'] for rank in data.get('availableRankings', [])]}")
|
||||||
|
logger.info(f"Latest season: {data.get('latestSeason', {})}")
|
||||||
|
logger.info(f"Latest week: {data.get('latestWeek', {})}")
|
||||||
|
|
||||||
|
rankings_data = data.get('rankings', [])
|
||||||
|
if not rankings_data:
|
||||||
|
logger.warning("No rankings data found")
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Use the first ranking (usually AP Top 25)
|
||||||
|
first_ranking = rankings_data[0]
|
||||||
|
ranking_name = first_ranking.get('name', 'Unknown')
|
||||||
|
ranking_type = first_ranking.get('type', 'Unknown')
|
||||||
|
teams = first_ranking.get('ranks', [])
|
||||||
|
|
||||||
|
logger.info(f"Using ranking: {ranking_name} ({ranking_type})")
|
||||||
|
logger.info(f"Found {len(teams)} teams in ranking")
|
||||||
|
|
||||||
|
standings = []
|
||||||
|
|
||||||
|
# Process each team in the ranking
|
||||||
|
for team_data in teams:
|
||||||
|
team_info = team_data.get('team', {})
|
||||||
|
team_name = team_info.get('name', 'Unknown')
|
||||||
|
team_abbr = team_info.get('abbreviation', 'Unknown')
|
||||||
|
current_rank = team_data.get('current', 0)
|
||||||
|
record_summary = team_data.get('recordSummary', '0-0')
|
||||||
|
|
||||||
|
logger.debug(f" {current_rank}. {team_name} ({team_abbr}): {record_summary}")
|
||||||
|
|
||||||
|
# Parse the record string (e.g., "12-1", "8-4", "10-2-1")
|
||||||
|
wins = 0
|
||||||
|
losses = 0
|
||||||
|
ties = 0
|
||||||
|
win_percentage = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
parts = record_summary.split('-')
|
||||||
|
if len(parts) >= 2:
|
||||||
|
wins = int(parts[0])
|
||||||
|
losses = int(parts[1])
|
||||||
|
if len(parts) == 3:
|
||||||
|
ties = int(parts[2])
|
||||||
|
|
||||||
|
# Calculate win percentage
|
||||||
|
total_games = wins + losses + ties
|
||||||
|
win_percentage = wins / total_games if total_games > 0 else 0
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
logger.warning(f"Could not parse record for {team_name}: {record_summary}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
standings.append({
|
||||||
|
'name': team_name,
|
||||||
|
'abbreviation': team_abbr,
|
||||||
|
'rank': current_rank,
|
||||||
|
'wins': wins,
|
||||||
|
'losses': losses,
|
||||||
|
'ties': ties,
|
||||||
|
'win_percentage': win_percentage,
|
||||||
|
'record_summary': record_summary,
|
||||||
|
'ranking_name': ranking_name
|
||||||
|
})
|
||||||
|
|
||||||
|
# Limit to top teams (they're already ranked)
|
||||||
|
top_teams = standings[:league_config['top_teams']]
|
||||||
|
|
||||||
|
# Cache the results
|
||||||
|
cache_data = {
|
||||||
|
'standings': top_teams,
|
||||||
|
'timestamp': time.time(),
|
||||||
|
'league': league_key,
|
||||||
|
'ranking_name': ranking_name
|
||||||
|
}
|
||||||
|
self.cache_manager.save_cache(cache_key, cache_data)
|
||||||
|
|
||||||
|
logger.info(f"Fetched and cached {len(top_teams)} teams for {league_key} using {ranking_name}")
|
||||||
|
return top_teams
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching rankings for {league_key}: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
def _fetch_standings_data(self, league_config: Dict[str, Any]) -> List[Dict[str, Any]]:
|
def _fetch_standings_data(self, league_config: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
"""Fetch standings data from ESPN API using the standings endpoint."""
|
"""Fetch standings data from ESPN API using the standings endpoint."""
|
||||||
league_key = league_config['league']
|
league_key = league_config['league']
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class LogoDownloader:
|
|||||||
'fcs': 'https://site.api.espn.com/apis/site/v2/sports/football/college-football/teams', # FCS teams from same endpoint
|
'fcs': 'https://site.api.espn.com/apis/site/v2/sports/football/college-football/teams', # FCS teams from same endpoint
|
||||||
'ncaam_basketball': 'https://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/teams',
|
'ncaam_basketball': 'https://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/teams',
|
||||||
'ncaa_baseball': 'https://site.api.espn.com/apis/site/v2/sports/baseball/college-baseball/teams',
|
'ncaa_baseball': 'https://site.api.espn.com/apis/site/v2/sports/baseball/college-baseball/teams',
|
||||||
|
'ncaam_hockey': 'https://site.api.espn.com/apis/site/v2/sports/hockey/mens-college-hockey/teams',
|
||||||
# Soccer leagues
|
# Soccer leagues
|
||||||
'soccer_eng.1': 'https://site.api.espn.com/apis/site/v2/sports/soccer/eng.1/teams',
|
'soccer_eng.1': 'https://site.api.espn.com/apis/site/v2/sports/soccer/eng.1/teams',
|
||||||
'soccer_esp.1': 'https://site.api.espn.com/apis/site/v2/sports/soccer/esp.1/teams',
|
'soccer_esp.1': 'https://site.api.espn.com/apis/site/v2/sports/soccer/esp.1/teams',
|
||||||
@@ -55,6 +56,7 @@ class LogoDownloader:
|
|||||||
'fcs': 'assets/sports/ncaa_logos', # FCS teams go in same directory
|
'fcs': 'assets/sports/ncaa_logos', # FCS teams go in same directory
|
||||||
'ncaam_basketball': 'assets/sports/ncaa_logos',
|
'ncaam_basketball': 'assets/sports/ncaa_logos',
|
||||||
'ncaa_baseball': 'assets/sports/ncaa_logos',
|
'ncaa_baseball': 'assets/sports/ncaa_logos',
|
||||||
|
'ncaam_hockey': 'assets/sports/ncaa_logos',
|
||||||
# Soccer leagues - all use the same soccer_logos directory
|
# Soccer leagues - all use the same soccer_logos directory
|
||||||
'soccer_eng.1': 'assets/sports/soccer_logos',
|
'soccer_eng.1': 'assets/sports/soccer_logos',
|
||||||
'soccer_esp.1': 'assets/sports/soccer_logos',
|
'soccer_esp.1': 'assets/sports/soccer_logos',
|
||||||
@@ -181,7 +183,7 @@ class LogoDownloader:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info(f"Fetching team data for {league} from ESPN API...")
|
logger.info(f"Fetching team data for {league} from ESPN API...")
|
||||||
response = self.session.get(api_url, headers=self.headers, timeout=self.request_timeout)
|
response = self.session.get(api_url, params={'limit':1000},headers=self.headers, timeout=self.request_timeout)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ class BaseNCAAFBManager: # Renamed class
|
|||||||
self._rankings_cache_timestamp = 0
|
self._rankings_cache_timestamp = 0
|
||||||
self._rankings_cache_duration = 3600 # Cache rankings for 1 hour
|
self._rankings_cache_duration = 3600 # Cache rankings for 1 hour
|
||||||
|
|
||||||
|
self.top_25_rankings = []
|
||||||
|
|
||||||
self.logger.info(f"Initialized NCAAFB manager with display dimensions: {self.display_width}x{self.display_height}")
|
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}")
|
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.logger.info(f"Display modes - Recent: {self.recent_enabled}, Upcoming: {self.upcoming_enabled}, Live: {self.live_enabled}")
|
||||||
@@ -190,7 +192,7 @@ class BaseNCAAFBManager: # Renamed class
|
|||||||
|
|
||||||
odds_data = self.odds_manager.get_odds(
|
odds_data = self.odds_manager.get_odds(
|
||||||
sport="football",
|
sport="football",
|
||||||
league="ncaa_fb",
|
league="college-football",
|
||||||
event_id=game['id'],
|
event_id=game['id'],
|
||||||
update_interval_seconds=update_interval
|
update_interval_seconds=update_interval
|
||||||
)
|
)
|
||||||
@@ -367,6 +369,39 @@ class BaseNCAAFBManager: # Renamed class
|
|||||||
else:
|
else:
|
||||||
return self._fetch_ncaa_fb_api_data(use_cache=True)
|
return self._fetch_ncaa_fb_api_data(use_cache=True)
|
||||||
|
|
||||||
|
def _fetch_rankings(self):
|
||||||
|
self.logger.info(f"[NCAAFB] Fetching current AP Top 25 rankings from ESPN API...")
|
||||||
|
try:
|
||||||
|
url = "http://site.api.espn.com/apis/site/v2/sports/football/college-football/rankings"
|
||||||
|
|
||||||
|
response = requests.get(url)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
# Grab rankings[0]
|
||||||
|
rankings_0 = data.get("rankings", [])[0]
|
||||||
|
|
||||||
|
# Extract top 25 team abbreviations
|
||||||
|
self.top_25_rankings = [
|
||||||
|
entry["team"]["abbreviation"]
|
||||||
|
for entry in rankings_0.get("ranks", [])[:25]
|
||||||
|
]
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
self.logger.error(f"[NCAAFB] Error retrieving AP Top 25 rankings: {e}")
|
||||||
|
|
||||||
|
def _get_rank(self, team_to_check):
|
||||||
|
i = 1
|
||||||
|
if self.top_25_rankings:
|
||||||
|
for team in self.top_25_rankings:
|
||||||
|
if team == team_to_check:
|
||||||
|
return i
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
def _load_fonts(self):
|
def _load_fonts(self):
|
||||||
"""Load fonts used by the scoreboard."""
|
"""Load fonts used by the scoreboard."""
|
||||||
fonts = {}
|
fonts = {}
|
||||||
@@ -376,6 +411,7 @@ class BaseNCAAFBManager: # Renamed class
|
|||||||
fonts['team'] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8)
|
fonts['team'] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8)
|
||||||
fonts['status'] = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6) # Using 4x6 for status
|
fonts['status'] = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6) # Using 4x6 for status
|
||||||
fonts['detail'] = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6) # Added detail font
|
fonts['detail'] = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6) # Added detail font
|
||||||
|
fonts['rank'] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 10)
|
||||||
logging.info("[NCAAFB] Successfully loaded fonts") # Changed log prefix
|
logging.info("[NCAAFB] Successfully loaded fonts") # Changed log prefix
|
||||||
except IOError:
|
except IOError:
|
||||||
logging.warning("[NCAAFB] Fonts not found, using default PIL font.") # Changed log prefix
|
logging.warning("[NCAAFB] Fonts not found, using default PIL font.") # Changed log prefix
|
||||||
@@ -384,6 +420,7 @@ class BaseNCAAFBManager: # Renamed class
|
|||||||
fonts['team'] = ImageFont.load_default()
|
fonts['team'] = ImageFont.load_default()
|
||||||
fonts['status'] = ImageFont.load_default()
|
fonts['status'] = ImageFont.load_default()
|
||||||
fonts['detail'] = ImageFont.load_default()
|
fonts['detail'] = ImageFont.load_default()
|
||||||
|
fonts['rank'] = ImageFont.load_default()
|
||||||
return fonts
|
return fonts
|
||||||
|
|
||||||
def _draw_dynamic_odds(self, draw: ImageDraw.Draw, odds: Dict[str, Any], width: int, height: int) -> None:
|
def _draw_dynamic_odds(self, draw: ImageDraw.Draw, odds: Dict[str, Any], width: int, height: int) -> None:
|
||||||
@@ -833,6 +870,9 @@ class NCAAFBLiveManager(BaseNCAAFBManager): # Renamed class
|
|||||||
self.logger.warning("[NCAAFB] Test mode: Could not parse clock") # Changed log prefix
|
self.logger.warning("[NCAAFB] Test mode: Could not parse clock") # Changed log prefix
|
||||||
# No actual display call here, let main loop handle it
|
# No actual display call here, let main loop handle it
|
||||||
else:
|
else:
|
||||||
|
# Fetch rankings
|
||||||
|
self._fetch_rankings()
|
||||||
|
|
||||||
# Fetch live game data
|
# Fetch live game data
|
||||||
data = self._fetch_data()
|
data = self._fetch_data()
|
||||||
new_live_games = []
|
new_live_games = []
|
||||||
@@ -960,6 +1000,24 @@ class NCAAFBLiveManager(BaseNCAAFBManager): # Renamed class
|
|||||||
main_img.paste(away_logo, (away_x, away_y), away_logo)
|
main_img.paste(away_logo, (away_x, away_y), away_logo)
|
||||||
|
|
||||||
# --- Draw Text Elements on Overlay ---
|
# --- Draw Text Elements on Overlay ---
|
||||||
|
# Ranking (if ranked)
|
||||||
|
home_rank = self._get_rank(game["home_abbr"])
|
||||||
|
away_rank = self._get_rank(game["away_abbr"])
|
||||||
|
|
||||||
|
if home_rank > 0:
|
||||||
|
rank_text = str(home_rank)
|
||||||
|
rank_width = draw_overlay.textlength(rank_text, font=self.fonts['rank'])
|
||||||
|
rank_x = home_x - 8
|
||||||
|
rank_y = 2
|
||||||
|
self._draw_text_with_outline(draw_overlay, rank_text, (rank_x, rank_y), self.fonts['rank'])
|
||||||
|
|
||||||
|
if away_rank > 0:
|
||||||
|
rank_text = str(away_rank)
|
||||||
|
rank_width = draw_overlay.textlength(rank_text, font=self.fonts['rank'])
|
||||||
|
rank_x = away_x + away_logo.width + 8
|
||||||
|
rank_y = 2
|
||||||
|
self._draw_text_with_outline(draw_overlay, rank_text, (rank_x, rank_y), self.fonts['rank'])
|
||||||
|
|
||||||
# Scores (centered, slightly above bottom)
|
# Scores (centered, slightly above bottom)
|
||||||
home_score = str(game.get("home_score", "0"))
|
home_score = str(game.get("home_score", "0"))
|
||||||
away_score = str(game.get("away_score", "0"))
|
away_score = str(game.get("away_score", "0"))
|
||||||
@@ -1103,6 +1161,9 @@ class NCAAFBRecentManager(BaseNCAAFBManager): # Renamed class
|
|||||||
self.last_update = current_time # Update time even if fetch fails
|
self.last_update = current_time # Update time even if fetch fails
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Fetch rankings
|
||||||
|
self._fetch_rankings()
|
||||||
|
|
||||||
data = self._fetch_data() # Uses shared cache
|
data = self._fetch_data() # Uses shared cache
|
||||||
if not data or 'events' not in data:
|
if not data or 'events' not in data:
|
||||||
self.logger.warning("[NCAAFB Recent] No events found in shared data.") # Changed log prefix
|
self.logger.warning("[NCAAFB Recent] No events found in shared data.") # Changed log prefix
|
||||||
@@ -1236,6 +1297,24 @@ class NCAAFBRecentManager(BaseNCAAFBManager): # Renamed class
|
|||||||
main_img.paste(away_logo, (away_x, away_y), away_logo)
|
main_img.paste(away_logo, (away_x, away_y), away_logo)
|
||||||
|
|
||||||
# Draw Text Elements on Overlay
|
# Draw Text Elements on Overlay
|
||||||
|
# Ranking (if ranked)
|
||||||
|
home_rank = self._get_rank(game["home_abbr"])
|
||||||
|
away_rank = self._get_rank(game["away_abbr"])
|
||||||
|
|
||||||
|
if home_rank > 0:
|
||||||
|
rank_text = str(home_rank)
|
||||||
|
rank_width = draw_overlay.textlength(rank_text, font=self.fonts['rank'])
|
||||||
|
rank_x = home_x - 8
|
||||||
|
rank_y = 2
|
||||||
|
self._draw_text_with_outline(draw_overlay, rank_text, (rank_x, rank_y), self.fonts['rank'])
|
||||||
|
|
||||||
|
if away_rank > 0:
|
||||||
|
rank_text = str(away_rank)
|
||||||
|
rank_width = draw_overlay.textlength(rank_text, font=self.fonts['rank'])
|
||||||
|
rank_x = away_x + away_logo.width - 8
|
||||||
|
rank_y = 2
|
||||||
|
self._draw_text_with_outline(draw_overlay, rank_text, (rank_x, rank_y), self.fonts['rank'])
|
||||||
|
|
||||||
# Final Scores (Centered, same position as live)
|
# Final Scores (Centered, same position as live)
|
||||||
home_score = str(game.get("home_score", "0"))
|
home_score = str(game.get("home_score", "0"))
|
||||||
away_score = str(game.get("away_score", "0"))
|
away_score = str(game.get("away_score", "0"))
|
||||||
@@ -1400,6 +1479,9 @@ class NCAAFBUpcomingManager(BaseNCAAFBManager): # Renamed class
|
|||||||
self.last_update = current_time
|
self.last_update = current_time
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Fetch rankings
|
||||||
|
self._fetch_rankings()
|
||||||
|
|
||||||
data = self._fetch_data() # Uses shared cache
|
data = self._fetch_data() # Uses shared cache
|
||||||
if not data or 'events' not in data:
|
if not data or 'events' not in data:
|
||||||
self.logger.warning("[NCAAFB Upcoming] No events found in shared data.") # Changed log prefix
|
self.logger.warning("[NCAAFB Upcoming] No events found in shared data.") # Changed log prefix
|
||||||
@@ -1587,6 +1669,25 @@ class NCAAFBUpcomingManager(BaseNCAAFBManager): # Renamed class
|
|||||||
game_date = game.get("game_date", "")
|
game_date = game.get("game_date", "")
|
||||||
game_time = game.get("game_time", "")
|
game_time = game.get("game_time", "")
|
||||||
|
|
||||||
|
# Ranking (if ranked)
|
||||||
|
home_rank = self._get_rank(game["home_abbr"])
|
||||||
|
away_rank = self._get_rank(game["away_abbr"])
|
||||||
|
|
||||||
|
if home_rank > 0:
|
||||||
|
rank_text = str(home_rank)
|
||||||
|
rank_width = draw_overlay.textlength(rank_text, font=self.fonts['rank'])
|
||||||
|
rank_x = home_x - 8
|
||||||
|
rank_y = 2
|
||||||
|
self._draw_text_with_outline(draw_overlay, rank_text, (rank_x, rank_y), self.fonts['rank'])
|
||||||
|
|
||||||
|
if away_rank > 0:
|
||||||
|
rank_text = str(away_rank)
|
||||||
|
rank_width = draw_overlay.textlength(rank_text, font=self.fonts['rank'])
|
||||||
|
rank_x = away_x + away_logo.width - 8
|
||||||
|
rank_y = 2
|
||||||
|
self._draw_text_with_outline(draw_overlay, rank_text, (rank_x, rank_y), self.fonts['rank'])
|
||||||
|
|
||||||
|
|
||||||
# "Next Game" at the top (use smaller status font)
|
# "Next Game" at the top (use smaller status font)
|
||||||
status_text = "Next Game"
|
status_text = "Next Game"
|
||||||
status_width = draw_overlay.textlength(status_text, font=self.fonts['status'])
|
status_width = draw_overlay.textlength(status_text, font=self.fonts['status'])
|
||||||
|
|||||||
954
src/ncaam_hockey_managers.py
Normal file
@@ -0,0 +1,954 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from src.display_manager import DisplayManager
|
||||||
|
from src.cache_manager import CacheManager # Keep CacheManager import
|
||||||
|
from src.odds_manager import OddsManager
|
||||||
|
from src.logo_downloader import download_missing_logo
|
||||||
|
import pytz
|
||||||
|
from requests.adapters import HTTPAdapter
|
||||||
|
from urllib3.util.retry import Retry
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
ESPN_NCAAMH_SCOREBOARD_URL = "https://site.api.espn.com/apis/site/v2/sports/hockey/mens-college-hockey/scoreboard" # Changed URL for NCAA FB
|
||||||
|
|
||||||
|
# Configure logging to match main configuration
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s.%(msecs)03d - %(levelname)s:%(name)s:%(message)s',
|
||||||
|
datefmt='%Y-%m-%d %H:%M:%S'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class BaseNCAAMHockeyManager: # Renamed class
|
||||||
|
"""Base class for NCAA Mens Hockey managers with common functionality.""" # Updated docstring
|
||||||
|
# Class variables for warning tracking
|
||||||
|
_no_data_warning_logged = False
|
||||||
|
_last_warning_time = 0
|
||||||
|
_warning_cooldown = 60 # Only log warnings once per minute
|
||||||
|
_shared_data = None
|
||||||
|
_last_shared_update = 0
|
||||||
|
_processed_games_cache = {} # Cache for processed game data
|
||||||
|
_processed_games_timestamp = 0
|
||||||
|
logger = logging.getLogger('NCAAMH') # Changed logger name
|
||||||
|
|
||||||
|
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager):
|
||||||
|
self.display_manager = display_manager
|
||||||
|
self.config = config
|
||||||
|
self.cache_manager = cache_manager
|
||||||
|
self.config_manager = self.cache_manager.config_manager
|
||||||
|
self.odds_manager = OddsManager(self.cache_manager, self.config_manager)
|
||||||
|
self.ncaam_hockey_config = config.get("ncaam_hockey_scoreboard", {}) # Changed config key
|
||||||
|
self.is_enabled = self.ncaam_hockey_config.get("enabled", False)
|
||||||
|
self.show_odds = self.ncaam_hockey_config.get("show_odds", False)
|
||||||
|
self.test_mode = self.ncaam_hockey_config.get("test_mode", False)
|
||||||
|
self.logo_dir = self.ncaam_hockey_config.get("logo_dir", "assets/sports/ncaa_logos") # Changed logo dir
|
||||||
|
self.update_interval = self.ncaam_hockey_config.get("update_interval_seconds", 60)
|
||||||
|
self.show_records = self.ncaam_hockey_config.get('show_records', False)
|
||||||
|
self.show_ranking = self.ncaam_hockey_config.get('show_ranking', False)
|
||||||
|
self.season_cache_duration = self.ncaam_hockey_config.get("season_cache_duration_seconds", 86400) # 24 hours default
|
||||||
|
# Number of games to show (instead of time-based windows)
|
||||||
|
self.recent_games_to_show = self.ncaam_hockey_config.get("recent_games_to_show", 5) # Show last 5 games
|
||||||
|
self.upcoming_games_to_show = self.ncaam_hockey_config.get("upcoming_games_to_show", 10) # Show next 10 games
|
||||||
|
|
||||||
|
# Set up session with retry logic
|
||||||
|
self.session = requests.Session()
|
||||||
|
retry_strategy = Retry(
|
||||||
|
total=5, # increased number of retries
|
||||||
|
backoff_factor=1, # increased backoff factor
|
||||||
|
status_forcelist=[429, 500, 502, 503, 504], # added 429 to retry list
|
||||||
|
allowed_methods=["GET", "HEAD", "OPTIONS"]
|
||||||
|
)
|
||||||
|
adapter = HTTPAdapter(max_retries=retry_strategy)
|
||||||
|
self.session.mount("https://", adapter)
|
||||||
|
self.session.mount("http://", adapter)
|
||||||
|
|
||||||
|
# Set up headers
|
||||||
|
self.headers = {
|
||||||
|
'User-Agent': 'LEDMatrix/1.0 (https://github.com/yourusername/LEDMatrix; contact@example.com)',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Accept-Language': 'en-US,en;q=0.9',
|
||||||
|
'Accept-Encoding': 'gzip, deflate, br',
|
||||||
|
'Connection': 'keep-alive'
|
||||||
|
}
|
||||||
|
self.last_update = 0
|
||||||
|
self.current_game = None
|
||||||
|
self.fonts = self._load_fonts()
|
||||||
|
self.favorite_teams = self.ncaam_hockey_config.get("favorite_teams", [])
|
||||||
|
|
||||||
|
# Check display modes to determine what data to fetch
|
||||||
|
display_modes = self.ncaam_hockey_config.get("display_modes", {})
|
||||||
|
self.recent_enabled = display_modes.get("ncaam_hockey_recent", False)
|
||||||
|
self.upcoming_enabled = display_modes.get("ncaam_hockey_upcoming", False)
|
||||||
|
self.live_enabled = display_modes.get("ncaam_hockey_live", False)
|
||||||
|
|
||||||
|
self.logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
self.display_width = self.display_manager.matrix.width
|
||||||
|
self.display_height = self.display_manager.matrix.height
|
||||||
|
|
||||||
|
self._logo_cache = {}
|
||||||
|
|
||||||
|
# Initialize team rankings cache
|
||||||
|
self._team_rankings_cache = {}
|
||||||
|
self._rankings_cache_timestamp = 0
|
||||||
|
self._rankings_cache_duration = 3600 # Cache rankings for 1 hour
|
||||||
|
|
||||||
|
self.logger.info(f"Initialized NCAAMHockey 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}")
|
||||||
|
|
||||||
|
def _fetch_team_rankings(self) -> Dict[str, int]:
|
||||||
|
"""Fetch current team rankings from ESPN API."""
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
# Check if we have cached rankings that are still valid
|
||||||
|
if (self._team_rankings_cache and
|
||||||
|
current_time - self._rankings_cache_timestamp < self._rankings_cache_duration):
|
||||||
|
return self._team_rankings_cache
|
||||||
|
|
||||||
|
try:
|
||||||
|
rankings_url = "https://site.api.espn.com/apis/site/v2/sports/hockey/mens-college-hockey/rankings"
|
||||||
|
response = self.session.get(rankings_url, headers=self.headers, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
rankings = {}
|
||||||
|
rankings_data = data.get('rankings', [])
|
||||||
|
|
||||||
|
if rankings_data:
|
||||||
|
# Use the first ranking (usually AP Top 25)
|
||||||
|
first_ranking = rankings_data[0]
|
||||||
|
teams = first_ranking.get('ranks', [])
|
||||||
|
|
||||||
|
for team_data in teams:
|
||||||
|
team_info = team_data.get('team', {})
|
||||||
|
team_abbr = team_info.get('abbreviation', '')
|
||||||
|
current_rank = team_data.get('current', 0)
|
||||||
|
|
||||||
|
if team_abbr and current_rank > 0:
|
||||||
|
rankings[team_abbr] = current_rank
|
||||||
|
|
||||||
|
# Cache the results
|
||||||
|
self._team_rankings_cache = rankings
|
||||||
|
self._rankings_cache_timestamp = current_time
|
||||||
|
|
||||||
|
self.logger.debug(f"Fetched rankings for {len(rankings)} teams")
|
||||||
|
return rankings
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error fetching team rankings: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _get_timezone(self):
|
||||||
|
try:
|
||||||
|
timezone_str = self.config.get('timezone', 'UTC')
|
||||||
|
return pytz.timezone(timezone_str)
|
||||||
|
except pytz.UnknownTimeZoneError:
|
||||||
|
return pytz.utc
|
||||||
|
|
||||||
|
def _should_log(self, warning_type: str, cooldown: int = 60) -> bool:
|
||||||
|
"""Check if we should log a warning based on cooldown period."""
|
||||||
|
current_time = time.time()
|
||||||
|
if current_time - self._last_warning_time > cooldown:
|
||||||
|
self._last_warning_time = current_time
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _fetch_odds(self, game: Dict) -> None:
|
||||||
|
"""Fetch odds for a specific game if conditions are met."""
|
||||||
|
# Check if odds should be shown for this sport
|
||||||
|
if not self.show_odds:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if we should only fetch for favorite teams
|
||||||
|
is_favorites_only = self.ncaam_hockey_config.get("show_favorite_teams_only", False)
|
||||||
|
if is_favorites_only:
|
||||||
|
home_abbr = game.get('home_abbr')
|
||||||
|
away_abbr = game.get('away_abbr')
|
||||||
|
if not (home_abbr in self.favorite_teams or away_abbr in self.favorite_teams):
|
||||||
|
self.logger.debug(f"Skipping odds fetch for non-favorite game in favorites-only mode: {away_abbr}@{home_abbr}")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.logger.debug(f"Proceeding with odds fetch for game: {game.get('id', 'N/A')}")
|
||||||
|
|
||||||
|
# Fetch odds using OddsManager (ESPN API)
|
||||||
|
try:
|
||||||
|
# Determine update interval based on game state
|
||||||
|
is_live = game.get('status', '').lower() == 'in'
|
||||||
|
update_interval = self.ncaam_hockey_config.get("live_odds_update_interval", 60) if is_live \
|
||||||
|
else self.ncaam_hockey_config.get("odds_update_interval", 3600)
|
||||||
|
|
||||||
|
odds_data = self.odds_manager.get_odds(
|
||||||
|
sport="hockey",
|
||||||
|
league="mens-college-hockey",
|
||||||
|
event_id=game['id'],
|
||||||
|
update_interval_seconds=update_interval
|
||||||
|
)
|
||||||
|
|
||||||
|
if odds_data:
|
||||||
|
game['odds'] = odds_data
|
||||||
|
self.logger.debug(f"Successfully fetched and attached odds for game {game['id']}")
|
||||||
|
else:
|
||||||
|
self.logger.debug(f"No odds data returned for game {game['id']}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error fetching odds for game {game.get('id', 'N/A')}: {e}")
|
||||||
|
|
||||||
|
def _fetch_ncaa_fb_api_data(self, use_cache: bool = True) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
Fetches the full season schedule for NCAAMH, caches it, and then filters
|
||||||
|
for relevant games based on the current configuration.
|
||||||
|
"""
|
||||||
|
now = datetime.now(pytz.utc)
|
||||||
|
current_year = now.year
|
||||||
|
years_to_check = [current_year]
|
||||||
|
if now.month < 8:
|
||||||
|
years_to_check.append(current_year - 1)
|
||||||
|
|
||||||
|
all_events = []
|
||||||
|
for year in years_to_check:
|
||||||
|
cache_key = f"ncaamh_schedule_{year}"
|
||||||
|
if use_cache:
|
||||||
|
cached_data = self.cache_manager.get(cache_key, max_age=self.season_cache_duration)
|
||||||
|
if cached_data:
|
||||||
|
self.logger.info(f"[NCAAMH] Using cached schedule for {year}")
|
||||||
|
all_events.extend(cached_data)
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.logger.info(f"[NCAAMH] Fetching full {year} season schedule from ESPN API...")
|
||||||
|
try:
|
||||||
|
response = self.session.get(ESPN_NCAAMH_SCOREBOARD_URL, params={"dates": year,"limit":1000},headers=self.headers, timeout=15)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
events = data.get('events', [])
|
||||||
|
if use_cache:
|
||||||
|
self.cache_manager.set(cache_key, events)
|
||||||
|
self.logger.info(f"[NCAAMH] Successfully fetched and cached {len(events)} events for {year} season.")
|
||||||
|
all_events.extend(events)
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
self.logger.error(f"[NCAAMH] API error fetching full schedule for {year}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not all_events:
|
||||||
|
self.logger.warning("[NCAAMH] No events found in schedule data.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {'events': all_events}
|
||||||
|
|
||||||
|
def _fetch_data(self, date_str: str = None) -> Optional[Dict]:
|
||||||
|
"""Fetch data using shared data mechanism or direct fetch for live."""
|
||||||
|
if isinstance(self, NCAAMHockeyLiveManager):
|
||||||
|
return self._fetch_ncaa_fb_api_data(use_cache=False)
|
||||||
|
else:
|
||||||
|
return self._fetch_ncaa_fb_api_data(use_cache=True)
|
||||||
|
|
||||||
|
def _load_fonts(self):
|
||||||
|
"""Load fonts used by the scoreboard."""
|
||||||
|
fonts = {}
|
||||||
|
try:
|
||||||
|
fonts['score'] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 12)
|
||||||
|
fonts['time'] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8)
|
||||||
|
fonts['team'] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8)
|
||||||
|
fonts['status'] = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6)
|
||||||
|
logging.info("[NCAAMH] Successfully loaded Press Start 2P font for all text elements")
|
||||||
|
except IOError:
|
||||||
|
logging.warning("[NCAAMH] Press Start 2P font not found, trying 4x6 font.")
|
||||||
|
try:
|
||||||
|
# Try to load the 4x6 font as a fallback
|
||||||
|
fonts['score'] = ImageFont.truetype("assets/fonts/4x6-font.ttf", 12)
|
||||||
|
fonts['time'] = ImageFont.truetype("assets/fonts/4x6-font.ttf", 8)
|
||||||
|
fonts['team'] = ImageFont.truetype("assets/fonts/4x6-font.ttf", 8)
|
||||||
|
fonts['status'] = ImageFont.truetype("assets/fonts/4x6-font.ttf", 9)
|
||||||
|
logging.info("[NCAAMH] Successfully loaded 4x6 font for all text elements")
|
||||||
|
except IOError:
|
||||||
|
logging.warning("[NCAAMH] 4x6 font not found, using default PIL font.")
|
||||||
|
# Use default PIL font as a last resort
|
||||||
|
fonts['score'] = ImageFont.load_default()
|
||||||
|
fonts['time'] = ImageFont.load_default()
|
||||||
|
fonts['team'] = ImageFont.load_default()
|
||||||
|
fonts['status'] = ImageFont.load_default()
|
||||||
|
return fonts
|
||||||
|
|
||||||
|
def _draw_text_with_outline(self, draw, text, position, font, fill=(255, 255, 255), outline_color=(0, 0, 0)):
|
||||||
|
"""
|
||||||
|
Draw text with a black outline for better readability.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
draw: ImageDraw object
|
||||||
|
text: Text to draw
|
||||||
|
position: (x, y) position to draw the text
|
||||||
|
font: Font to use
|
||||||
|
fill: Text color (default: white)
|
||||||
|
outline_color: Outline color (default: black)
|
||||||
|
"""
|
||||||
|
x, y = position
|
||||||
|
|
||||||
|
# Draw the outline by drawing the text in black at 8 positions around the text
|
||||||
|
for dx, dy in [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]:
|
||||||
|
draw.text((x + dx, y + dy), text, font=font, fill=outline_color)
|
||||||
|
|
||||||
|
# Draw the text in the specified color
|
||||||
|
draw.text((x, y), text, font=font, fill=fill)
|
||||||
|
|
||||||
|
def _load_and_resize_logo(self, team_abbrev: str, team_name: str = None) -> Optional[Image.Image]:
|
||||||
|
"""Load and resize a team logo, with caching and automatic download if missing."""
|
||||||
|
if team_abbrev in self._logo_cache:
|
||||||
|
return self._logo_cache[team_abbrev]
|
||||||
|
|
||||||
|
logo_path = os.path.join(self.logo_dir, f"{team_abbrev}.png")
|
||||||
|
self.logger.debug(f"Logo path: {logo_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Try to download missing logo first
|
||||||
|
if not os.path.exists(logo_path):
|
||||||
|
self.logger.info(f"Logo not found for {team_abbrev} at {logo_path}. Attempting to download.")
|
||||||
|
|
||||||
|
# Try to download the logo from ESPN API
|
||||||
|
success = download_missing_logo(team_abbrev, 'ncaam_hockey', team_name)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
# Create placeholder if download fails
|
||||||
|
self.logger.warning(f"Failed to download logo for {team_abbrev}. Creating placeholder.")
|
||||||
|
os.makedirs(os.path.dirname(logo_path), exist_ok=True)
|
||||||
|
logo = Image.new('RGBA', (32, 32), (200, 200, 200, 255)) # Gray placeholder
|
||||||
|
draw = ImageDraw.Draw(logo)
|
||||||
|
draw.text((2, 10), team_abbrev, fill=(0, 0, 0, 255))
|
||||||
|
logo.save(logo_path)
|
||||||
|
self.logger.info(f"Created placeholder logo at {logo_path}")
|
||||||
|
|
||||||
|
logo = Image.open(logo_path)
|
||||||
|
if logo.mode != 'RGBA':
|
||||||
|
logo = logo.convert('RGBA')
|
||||||
|
|
||||||
|
max_width = int(self.display_width * 1.5)
|
||||||
|
max_height = int(self.display_height * 1.5)
|
||||||
|
logo.thumbnail((max_width, max_height), Image.Resampling.LANCZOS)
|
||||||
|
self._logo_cache[team_abbrev] = logo
|
||||||
|
return logo
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error loading logo for {team_abbrev}: {e}", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _extract_game_details(self, game_event: Dict) -> Optional[Dict]:
|
||||||
|
"""Extract relevant game details from ESPN API response."""
|
||||||
|
if not game_event:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
competition = game_event["competitions"][0]
|
||||||
|
status = competition["status"]
|
||||||
|
competitors = competition["competitors"]
|
||||||
|
game_date_str = game_event["date"]
|
||||||
|
|
||||||
|
# Parse game date/time
|
||||||
|
try:
|
||||||
|
start_time_utc = datetime.fromisoformat(game_date_str.replace("Z", "+00:00"))
|
||||||
|
self.logger.debug(f"[NCAAMH] Parsed game time: {start_time_utc}")
|
||||||
|
except ValueError:
|
||||||
|
logging.warning(f"[NCAAMH] Could not parse game date: {game_date_str}")
|
||||||
|
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")
|
||||||
|
home_record = home_team.get('records', [{}])[0].get('summary', '') if home_team.get('records') else ''
|
||||||
|
away_record = away_team.get('records', [{}])[0].get('summary', '') if away_team.get('records') else ''
|
||||||
|
|
||||||
|
# Don't show "0-0" records - set to blank instead
|
||||||
|
if home_record == "0-0":
|
||||||
|
home_record = ''
|
||||||
|
if away_record == "0-0":
|
||||||
|
away_record = ''
|
||||||
|
|
||||||
|
# Format game time and date for display
|
||||||
|
game_time = ""
|
||||||
|
game_date = ""
|
||||||
|
if start_time_utc:
|
||||||
|
# Convert to local time
|
||||||
|
local_time = start_time_utc.astimezone(self._get_timezone())
|
||||||
|
game_time = local_time.strftime("%-I:%M%p")
|
||||||
|
|
||||||
|
# Check date format from config
|
||||||
|
use_short_date_format = self.config.get('display', {}).get('use_short_date_format', False)
|
||||||
|
if use_short_date_format:
|
||||||
|
game_date = local_time.strftime("%-m/%-d")
|
||||||
|
else:
|
||||||
|
game_date = self.display_manager.format_date_with_ordinal(local_time)
|
||||||
|
|
||||||
|
details = {
|
||||||
|
"start_time_utc": start_time_utc,
|
||||||
|
"status_text": status["type"]["shortDetail"],
|
||||||
|
"period": status.get("period", 0),
|
||||||
|
"clock": status.get("displayClock", "0:00"),
|
||||||
|
"is_live": status["type"]["state"] in ("in", "halftime"),
|
||||||
|
"is_final": status["type"]["state"] == "post",
|
||||||
|
"is_upcoming": status["type"]["state"] == "pre",
|
||||||
|
"home_abbr": home_team["team"]["abbreviation"],
|
||||||
|
"home_score": home_team.get("score", "0"),
|
||||||
|
"home_record": home_record,
|
||||||
|
"home_logo_path": os.path.join(self.logo_dir, f"{home_team['team']['abbreviation']}.png"),
|
||||||
|
"away_abbr": away_team["team"]["abbreviation"],
|
||||||
|
"away_score": away_team.get("score", "0"),
|
||||||
|
"away_record": away_record,
|
||||||
|
"away_logo_path": os.path.join(self.logo_dir, f"{away_team['team']['abbreviation']}.png"),
|
||||||
|
"game_time": game_time,
|
||||||
|
"game_date": game_date,
|
||||||
|
"id": game_event.get("id")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Log game details for debugging
|
||||||
|
self.logger.debug(f"[NCAAMH] Extracted game details: {details['away_abbr']} vs {details['home_abbr']}")
|
||||||
|
# Use .get() to avoid KeyError if optional keys are missing
|
||||||
|
self.logger.debug(
|
||||||
|
f"[NCAAMH] Game status: is_final={details.get('is_final')}, "
|
||||||
|
f"is_upcoming={details.get('is_upcoming')}, is_live={details.get('is_live')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate logo files
|
||||||
|
for team in ["home", "away"]:
|
||||||
|
logo_path = details[f"{team}_logo_path"]
|
||||||
|
if not os.path.isfile(logo_path):
|
||||||
|
# logging.warning(f"[NCAAMH] {team.title()} logo not found: {logo_path}")
|
||||||
|
details[f"{team}_logo_path"] = None
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
with Image.open(logo_path) as img:
|
||||||
|
logging.debug(f"[NCAAMH] {team.title()} logo is valid: {img.format}, size: {img.size}")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"[NCAAMH] {team.title()} logo file exists but is not valid: {e}")
|
||||||
|
details[f"{team}_logo_path"] = None
|
||||||
|
|
||||||
|
return details
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"[NCAAMH] Error extracting game details: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _draw_scorebug_layout(self, game: Dict, force_clear: bool = False) -> None:
|
||||||
|
"""Draw the scorebug layout for the current game."""
|
||||||
|
try:
|
||||||
|
# Create a new black image for the main display
|
||||||
|
main_img = Image.new('RGBA', (self.display_width, self.display_height), (0, 0, 0, 255))
|
||||||
|
|
||||||
|
# Load logos once
|
||||||
|
home_logo = self._load_and_resize_logo(game["home_abbr"])
|
||||||
|
away_logo = self._load_and_resize_logo(game["away_abbr"])
|
||||||
|
|
||||||
|
if not home_logo or not away_logo:
|
||||||
|
self.logger.error("Failed to load one or both team logos")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create a single overlay for both logos
|
||||||
|
overlay = Image.new('RGBA', (self.display_width, self.display_height), (0, 0, 0, 0))
|
||||||
|
|
||||||
|
# Calculate vertical center line for alignment
|
||||||
|
center_y = self.display_height // 2
|
||||||
|
|
||||||
|
# Draw home team logo (far right, extending beyond screen)
|
||||||
|
home_x = self.display_width - home_logo.width + 2
|
||||||
|
home_y = center_y - (home_logo.height // 2)
|
||||||
|
|
||||||
|
# Paste the home logo onto the overlay
|
||||||
|
overlay.paste(home_logo, (home_x, home_y), home_logo)
|
||||||
|
|
||||||
|
# Draw away team logo (far left, extending beyond screen)
|
||||||
|
away_x = -2
|
||||||
|
away_y = center_y - (away_logo.height // 2)
|
||||||
|
|
||||||
|
# Paste the away logo onto the overlay
|
||||||
|
overlay.paste(away_logo, (away_x, away_y), away_logo)
|
||||||
|
|
||||||
|
# Composite the overlay with the main image
|
||||||
|
main_img = Image.alpha_composite(main_img, overlay)
|
||||||
|
|
||||||
|
# Convert to RGB for final display
|
||||||
|
main_img = main_img.convert('RGB')
|
||||||
|
draw = ImageDraw.Draw(main_img)
|
||||||
|
|
||||||
|
# Check if this is an upcoming game
|
||||||
|
is_upcoming = game.get("is_upcoming", False)
|
||||||
|
|
||||||
|
if is_upcoming:
|
||||||
|
# For upcoming games, show date and time stacked in the center
|
||||||
|
game_date = game.get("game_date", "")
|
||||||
|
game_time = game.get("game_time", "")
|
||||||
|
|
||||||
|
# Show "Next Game" at the top
|
||||||
|
status_text = "Next Game"
|
||||||
|
status_width = draw.textlength(status_text, font=self.fonts['status'])
|
||||||
|
status_x = (self.display_width - status_width) // 2
|
||||||
|
status_y = 2
|
||||||
|
self._draw_text_with_outline(draw, status_text, (status_x, status_y), self.fonts['status'])
|
||||||
|
|
||||||
|
# Calculate position for the date text (centered horizontally, below "Next Game")
|
||||||
|
date_width = draw.textlength(game_date, font=self.fonts['time'])
|
||||||
|
date_x = (self.display_width - date_width) // 2
|
||||||
|
date_y = center_y - 5 # Position in center
|
||||||
|
self._draw_text_with_outline(draw, game_date, (date_x, date_y), self.fonts['time'])
|
||||||
|
|
||||||
|
# Calculate position for the time text (centered horizontally, in center)
|
||||||
|
time_width = draw.textlength(game_time, font=self.fonts['time'])
|
||||||
|
time_x = (self.display_width - time_width) // 2
|
||||||
|
time_y = date_y + 10 # Position below date
|
||||||
|
self._draw_text_with_outline(draw, game_time, (time_x, time_y), self.fonts['time'])
|
||||||
|
else:
|
||||||
|
# For live/final games, show scores and period/time
|
||||||
|
home_score = str(game.get("home_score", "0"))
|
||||||
|
away_score = str(game.get("away_score", "0"))
|
||||||
|
score_text = f"{away_score}-{home_score}"
|
||||||
|
|
||||||
|
# Calculate position for the score text (centered at the bottom)
|
||||||
|
score_width = draw.textlength(score_text, font=self.fonts['score'])
|
||||||
|
score_x = (self.display_width - score_width) // 2
|
||||||
|
score_y = self.display_height - 15
|
||||||
|
self._draw_text_with_outline(draw, score_text, (score_x, score_y), self.fonts['score'])
|
||||||
|
|
||||||
|
# Draw period and time or Final
|
||||||
|
if game.get("is_final", False):
|
||||||
|
status_text = "Final"
|
||||||
|
else:
|
||||||
|
period = game.get("period", 0)
|
||||||
|
clock = game.get("clock", "0:00")
|
||||||
|
|
||||||
|
# Format period text
|
||||||
|
if period > 3:
|
||||||
|
period_text = "OT"
|
||||||
|
else:
|
||||||
|
period_text = f"{period}{'st' if period == 1 else 'nd' if period == 2 else 'rd'}"
|
||||||
|
|
||||||
|
status_text = f"{period_text} {clock}"
|
||||||
|
|
||||||
|
# Calculate position for the status text (centered at the top)
|
||||||
|
status_width = draw.textlength(status_text, font=self.fonts['time'])
|
||||||
|
status_x = (self.display_width - status_width) // 2
|
||||||
|
status_y = 5
|
||||||
|
self._draw_text_with_outline(draw, status_text, (status_x, status_y), self.fonts['time'])
|
||||||
|
|
||||||
|
# Display odds if available
|
||||||
|
if 'odds' in game:
|
||||||
|
odds = game['odds']
|
||||||
|
spread = odds.get('spread', {}).get('point', None)
|
||||||
|
if spread is not None:
|
||||||
|
# Format spread text
|
||||||
|
spread_text = f"{spread:+.1f}" if spread > 0 else f"{spread:.1f}"
|
||||||
|
|
||||||
|
# Choose color and position based on which team has the spread
|
||||||
|
if odds.get('spread', {}).get('team') == game['home_abbr']:
|
||||||
|
text_color = (255, 100, 100) # Reddish
|
||||||
|
spread_x = self.display_width - draw.textlength(spread_text, font=self.fonts['status']) - 2
|
||||||
|
else:
|
||||||
|
text_color = (100, 255, 100) # Greenish
|
||||||
|
spread_x = 2
|
||||||
|
|
||||||
|
spread_y = 0
|
||||||
|
self._draw_text_with_outline(draw, spread_text, (spread_x, spread_y), self.fonts['status'], fill=text_color)
|
||||||
|
|
||||||
|
# Draw records if enabled
|
||||||
|
if self.show_records:
|
||||||
|
try:
|
||||||
|
record_font = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6)
|
||||||
|
except IOError:
|
||||||
|
record_font = ImageFont.load_default()
|
||||||
|
|
||||||
|
away_record = game.get('away_record', '')
|
||||||
|
home_record = game.get('home_record', '')
|
||||||
|
|
||||||
|
record_bbox = draw.textbbox((0,0), "0-0", font=record_font)
|
||||||
|
record_height = record_bbox[3] - record_bbox[1]
|
||||||
|
record_y = self.display_height - record_height
|
||||||
|
|
||||||
|
if away_record:
|
||||||
|
away_record_x = 2
|
||||||
|
self._draw_text_with_outline(draw, away_record, (away_record_x, record_y), record_font)
|
||||||
|
|
||||||
|
if home_record:
|
||||||
|
home_record_bbox = draw.textbbox((0,0), home_record, font=record_font)
|
||||||
|
home_record_width = home_record_bbox[2] - home_record_bbox[0]
|
||||||
|
home_record_x = self.display_width - home_record_width - 2
|
||||||
|
self._draw_text_with_outline(draw, home_record, (home_record_x, record_y), record_font)
|
||||||
|
|
||||||
|
# Display the image
|
||||||
|
self.display_manager.image.paste(main_img, (0, 0))
|
||||||
|
self.display_manager.update_display()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error displaying game: {e}", exc_info=True)
|
||||||
|
|
||||||
|
def display(self, force_clear: bool = False) -> None:
|
||||||
|
"""Common display method for all NCAAMH managers"""
|
||||||
|
if not self.current_game:
|
||||||
|
current_time = time.time()
|
||||||
|
if not hasattr(self, '_last_warning_time'):
|
||||||
|
self._last_warning_time = 0
|
||||||
|
if current_time - self._last_warning_time > 300: # 5 minutes cooldown
|
||||||
|
self.logger.warning("[NCAAMH] No game data available to display")
|
||||||
|
self._last_warning_time = current_time
|
||||||
|
return
|
||||||
|
|
||||||
|
self._draw_scorebug_layout(self.current_game, force_clear)
|
||||||
|
|
||||||
|
class NCAAMHockeyLiveManager(BaseNCAAMHockeyManager): # Renamed class
|
||||||
|
"""Manager for live NCAA Mens Hockey games."""
|
||||||
|
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager):
|
||||||
|
super().__init__(config, display_manager, cache_manager)
|
||||||
|
self.update_interval = self.ncaam_hockey_config.get("live_update_interval", 15) # 15 seconds for live games
|
||||||
|
self.no_data_interval = 300 # 5 minutes when no live games
|
||||||
|
self.last_update = 0
|
||||||
|
self.logger.info("Initialized NCAA Mens Hockey 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.ncaam_hockey_config.get("live_game_duration", 20) # Display each live game for 20 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
|
||||||
|
|
||||||
|
# Initialize with test game only if test mode is enabled
|
||||||
|
if self.test_mode:
|
||||||
|
self.current_game = {
|
||||||
|
"home_abbr": "RIT",
|
||||||
|
"away_abbr": "PU",
|
||||||
|
"home_score": "3",
|
||||||
|
"away_score": "2",
|
||||||
|
"period": 2,
|
||||||
|
"clock": "12:34",
|
||||||
|
"home_logo_path": os.path.join(self.logo_dir, "RIT.png"),
|
||||||
|
"away_logo_path": os.path.join(self.logo_dir, "PU.png"),
|
||||||
|
"game_time": "7:30 PM",
|
||||||
|
"game_date": "Apr 17"
|
||||||
|
}
|
||||||
|
self.live_games = [self.current_game]
|
||||||
|
logging.info("[NCAAMH] Initialized NCAAMHockeyLiveManager with test game: RIT vs PU")
|
||||||
|
else:
|
||||||
|
logging.info("[NCAAMH] Initialized NCAAMHockeyLiveManager in live mode")
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Update live game data."""
|
||||||
|
if not self.is_enabled: return
|
||||||
|
current_time = time.time()
|
||||||
|
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
|
||||||
|
|
||||||
|
if self.test_mode:
|
||||||
|
# For testing, we'll just update the clock to show it's working
|
||||||
|
if self.current_game:
|
||||||
|
minutes = int(self.current_game["clock"].split(":")[0])
|
||||||
|
seconds = int(self.current_game["clock"].split(":")[1])
|
||||||
|
seconds -= 1
|
||||||
|
if seconds < 0:
|
||||||
|
seconds = 59
|
||||||
|
minutes -= 1
|
||||||
|
if minutes < 0:
|
||||||
|
minutes = 19
|
||||||
|
if self.current_game["period"] < 3:
|
||||||
|
self.current_game["period"] += 1
|
||||||
|
else:
|
||||||
|
self.current_game["period"] = 1
|
||||||
|
self.current_game["clock"] = f"{minutes:02d}:{seconds:02d}"
|
||||||
|
# Always update display in test mode
|
||||||
|
self.display(force_clear=True)
|
||||||
|
else:
|
||||||
|
# Fetch live game data from ESPN API
|
||||||
|
data = self._fetch_data()
|
||||||
|
if data and "events" in data:
|
||||||
|
# Find all live games involving favorite teams
|
||||||
|
new_live_games = []
|
||||||
|
for event in data["events"]:
|
||||||
|
details = self._extract_game_details(event)
|
||||||
|
if details and details["is_live"]:
|
||||||
|
self._fetch_odds(details)
|
||||||
|
new_live_games.append(details)
|
||||||
|
|
||||||
|
# Filter for favorite teams only if the config is set
|
||||||
|
if self.ncaam_hockey_config.get("show_favorite_teams_only", False):
|
||||||
|
new_live_games = [game for game in new_live_games
|
||||||
|
if game['home_abbr'] in self.favorite_teams or
|
||||||
|
game['away_abbr'] in self.favorite_teams]
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
filter_text = "favorite teams" if self.ncaam_hockey_config.get("show_favorite_teams_only", False) else "all teams"
|
||||||
|
self.logger.info(f"[NCAAMH] Found {len(new_live_games)} live games involving {filter_text}")
|
||||||
|
for game in new_live_games:
|
||||||
|
self.logger.info(f"[NCAAMH] Live game: {game['away_abbr']} vs {game['home_abbr']} - Period {game['period']}, {game['clock']}")
|
||||||
|
else:
|
||||||
|
filter_text = "favorite teams" if self.ncaam_hockey_config.get("show_favorite_teams_only", False) else "criteria"
|
||||||
|
self.logger.info(f"[NCAAMH] No live games found matching {filter_text}")
|
||||||
|
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_abbr"] == self.current_game["home_abbr"] and
|
||||||
|
new_game["away_abbr"] == self.current_game["away_abbr"]) or
|
||||||
|
(new_game["home_abbr"] == self.current_game["away_abbr"] and
|
||||||
|
new_game["away_abbr"] == self.current_game["home_abbr"])
|
||||||
|
):
|
||||||
|
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_abbr"] + game["home_abbr"] for game in new_live_games) != set(game["away_abbr"] + game["home_abbr"] 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
|
||||||
|
|
||||||
|
# Update display if data changed, limit rate
|
||||||
|
if current_time - self.last_display_update >= 1.0:
|
||||||
|
# self.display(force_clear=True) # REMOVED: DisplayController handles this
|
||||||
|
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
|
||||||
|
# self.display(force_clear=True) # REMOVED: DisplayController handles this
|
||||||
|
self.last_display_update = current_time # Track time for potential display update
|
||||||
|
|
||||||
|
def display(self, force_clear=False):
|
||||||
|
"""Display live game information."""
|
||||||
|
if not self.current_game:
|
||||||
|
return
|
||||||
|
super().display(force_clear) # Call parent class's display method
|
||||||
|
|
||||||
|
|
||||||
|
class NCAAMHockeyRecentManager(BaseNCAAMHockeyManager):
|
||||||
|
"""Manager for recently completed NCAAMH games."""
|
||||||
|
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager):
|
||||||
|
super().__init__(config, display_manager, cache_manager)
|
||||||
|
self.recent_games = []
|
||||||
|
self.current_game_index = 0
|
||||||
|
self.last_update = 0
|
||||||
|
self.update_interval = 300 # 5 minutes
|
||||||
|
self.recent_games_to_show = self.ncaam_hockey_config.get("recent_games_to_show", 5) # Number of most recent games to display
|
||||||
|
self.last_game_switch = 0
|
||||||
|
self.game_display_duration = 15 # Display each game for 15 seconds
|
||||||
|
self.logger.info(f"Initialized NCAAMHRecentManager 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
|
||||||
|
|
||||||
|
self.last_update = current_time
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Fetch data from ESPN API
|
||||||
|
data = self._fetch_data()
|
||||||
|
if not data or 'events' not in data:
|
||||||
|
self.logger.warning("[NCAAMH] No events found in ESPN API response")
|
||||||
|
return
|
||||||
|
|
||||||
|
events = data['events']
|
||||||
|
self.logger.info(f"[NCAAMH] Successfully fetched {len(events)} events from ESPN API")
|
||||||
|
|
||||||
|
# Process games
|
||||||
|
processed_games = []
|
||||||
|
for event in events:
|
||||||
|
game = self._extract_game_details(event)
|
||||||
|
if game and game['is_final']:
|
||||||
|
# Fetch odds if enabled
|
||||||
|
self._fetch_odds(game)
|
||||||
|
processed_games.append(game)
|
||||||
|
|
||||||
|
# Filter for favorite teams only if the config is set
|
||||||
|
if self.ncaam_hockey_config.get("show_favorite_teams_only", False):
|
||||||
|
team_games = [game for game in processed_games
|
||||||
|
if game['home_abbr'] in self.favorite_teams or
|
||||||
|
game['away_abbr'] in self.favorite_teams]
|
||||||
|
else:
|
||||||
|
team_games = processed_games
|
||||||
|
|
||||||
|
# Sort games by start time, most recent first, then limit to recent_games_to_show
|
||||||
|
team_games.sort(key=lambda x: x.get('start_time_utc') or datetime.min.replace(tzinfo=timezone.utc), reverse=True)
|
||||||
|
team_games = team_games[:self.recent_games_to_show]
|
||||||
|
|
||||||
|
self.logger.info(f"[NCAAMH] Found {len(team_games)} recent games for favorite teams (limited to {self.recent_games_to_show})")
|
||||||
|
|
||||||
|
new_game_ids = {g['id'] for g in team_games}
|
||||||
|
current_game_ids = {g['id'] for g in getattr(self, 'games_list', [])}
|
||||||
|
|
||||||
|
if new_game_ids != current_game_ids:
|
||||||
|
self.games_list = team_games
|
||||||
|
self.current_game_index = 0
|
||||||
|
self.current_game = self.games_list[0] if self.games_list else None
|
||||||
|
self.last_game_switch = current_time
|
||||||
|
elif self.games_list:
|
||||||
|
self.current_game = self.games_list[self.current_game_index]
|
||||||
|
|
||||||
|
if not self.games_list:
|
||||||
|
self.current_game = None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"[NCAAMH] Error updating recent games: {e}", exc_info=True)
|
||||||
|
|
||||||
|
def display(self, force_clear=False):
|
||||||
|
"""Display recent games."""
|
||||||
|
if not self.games_list:
|
||||||
|
self.logger.info("[NCAAMH] No recent games to display")
|
||||||
|
return # Skip display update entirely
|
||||||
|
|
||||||
|
try:
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
# Check if it's time to switch games
|
||||||
|
if len(self.games_list) > 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.games_list)
|
||||||
|
self.current_game = self.games_list[self.current_game_index]
|
||||||
|
self.last_game_switch = current_time
|
||||||
|
force_clear = True # Force clear when switching games
|
||||||
|
|
||||||
|
# Draw the scorebug layout
|
||||||
|
self._draw_scorebug_layout(self.current_game, force_clear)
|
||||||
|
|
||||||
|
# Update display
|
||||||
|
self.display_manager.update_display()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"[NCAAMH] Error displaying recent game: {e}", exc_info=True)
|
||||||
|
|
||||||
|
class NCAAMHockeyUpcomingManager(BaseNCAAMHockeyManager):
|
||||||
|
"""Manager for upcoming NCAA Mens Hockey games."""
|
||||||
|
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager):
|
||||||
|
super().__init__(config, display_manager, cache_manager)
|
||||||
|
self.upcoming_games = []
|
||||||
|
self.current_game_index = 0
|
||||||
|
self.last_update = 0
|
||||||
|
self.update_interval = 300 # 5 minutes
|
||||||
|
self.upcoming_games_to_show = self.ncaam_hockey_config.get("upcoming_games_to_show", 5) # Number of upcoming games to display
|
||||||
|
self.last_log_time = 0
|
||||||
|
self.log_interval = 300 # Only log status every 5 minutes
|
||||||
|
self.last_warning_time = 0
|
||||||
|
self.warning_cooldown = 300 # Only show warning every 5 minutes
|
||||||
|
self.last_game_switch = 0 # Track when we last switched games
|
||||||
|
self.game_display_duration = 15 # Display each game for 15 seconds
|
||||||
|
self.logger.info(f"Initialized NCAAMHUpcomingManager 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
|
||||||
|
|
||||||
|
self.last_update = current_time
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Fetch data from ESPN API
|
||||||
|
data = self._fetch_data()
|
||||||
|
if not data or 'events' not in data:
|
||||||
|
self.logger.warning("[NCAAMH] No events found in ESPN API response")
|
||||||
|
return
|
||||||
|
|
||||||
|
events = data['events']
|
||||||
|
self.logger.info(f"[NCAAMH] Successfully fetched {len(events)} events from ESPN API")
|
||||||
|
|
||||||
|
# Process games
|
||||||
|
new_upcoming_games = []
|
||||||
|
for event in events:
|
||||||
|
game = self._extract_game_details(event)
|
||||||
|
if game and game['is_upcoming']:
|
||||||
|
# Only fetch odds for games that will be displayed
|
||||||
|
if self.ncaam_hockey_config.get("show_favorite_teams_only", False):
|
||||||
|
if not self.favorite_teams or (game['home_abbr'] not in self.favorite_teams and game['away_abbr'] not in self.favorite_teams):
|
||||||
|
continue
|
||||||
|
|
||||||
|
self._fetch_odds(game)
|
||||||
|
new_upcoming_games.append(game)
|
||||||
|
|
||||||
|
# Filter for favorite teams only if the config is set
|
||||||
|
if self.ncaam_hockey_config.get("show_favorite_teams_only", False):
|
||||||
|
team_games = [game for game in new_upcoming_games
|
||||||
|
if game['home_abbr'] in self.favorite_teams or
|
||||||
|
game['away_abbr'] in self.favorite_teams]
|
||||||
|
else:
|
||||||
|
team_games = new_upcoming_games
|
||||||
|
|
||||||
|
# Sort games by start time, soonest first, then limit to configured count
|
||||||
|
team_games.sort(key=lambda x: x.get('start_time_utc') or datetime.max.replace(tzinfo=timezone.utc))
|
||||||
|
team_games = team_games[:self.upcoming_games_to_show]
|
||||||
|
|
||||||
|
# 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(team_games) != len(self.upcoming_games) or
|
||||||
|
not self.upcoming_games # Log if we had no games before
|
||||||
|
)
|
||||||
|
|
||||||
|
if should_log:
|
||||||
|
if team_games:
|
||||||
|
self.logger.info(f"[NCAAMH] Found {len(team_games)} upcoming games for favorite teams (limited to {self.upcoming_games_to_show})")
|
||||||
|
for game in team_games:
|
||||||
|
self.logger.info(f"[NCAAMH] Upcoming game: {game['away_abbr']} vs {game['home_abbr']} - {game['game_date']} {game['game_time']}")
|
||||||
|
else:
|
||||||
|
self.logger.info("[NCAAMH] No upcoming games found for favorite teams")
|
||||||
|
self.logger.debug(f"[NCAAMH] Favorite teams: {self.favorite_teams}")
|
||||||
|
self.last_log_time = current_time
|
||||||
|
|
||||||
|
self.upcoming_games = team_games
|
||||||
|
if self.upcoming_games:
|
||||||
|
if not self.current_game or self.current_game['id'] not in {g['id'] for g in self.upcoming_games}:
|
||||||
|
self.current_game_index = 0
|
||||||
|
self.current_game = self.upcoming_games[0]
|
||||||
|
self.last_game_switch = current_time
|
||||||
|
else:
|
||||||
|
self.current_game = None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"[NCAAMH] Error updating upcoming games: {e}", exc_info=True)
|
||||||
|
|
||||||
|
def display(self, force_clear=False):
|
||||||
|
"""Display upcoming games."""
|
||||||
|
if not self.upcoming_games:
|
||||||
|
current_time = time.time()
|
||||||
|
if current_time - self.last_warning_time > self.warning_cooldown:
|
||||||
|
self.logger.info("[NCAAMH] No upcoming 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 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 # Force clear when switching games
|
||||||
|
|
||||||
|
# Draw the scorebug layout
|
||||||
|
self._draw_scorebug_layout(self.current_game, force_clear)
|
||||||
|
|
||||||
|
# Update display
|
||||||
|
self.display_manager.update_display()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"[NCAAMH] Error displaying upcoming game: {e}", exc_info=True)
|
||||||
@@ -154,8 +154,8 @@ class BaseNFLManager: # Renamed class
|
|||||||
|
|
||||||
self.logger.info(f"[NFL] Fetching full {current_year} season schedule from ESPN API (cache_enabled={use_cache})...")
|
self.logger.info(f"[NFL] Fetching full {current_year} season schedule from ESPN API (cache_enabled={use_cache})...")
|
||||||
try:
|
try:
|
||||||
url = f"https://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard?dates={current_year}"
|
url = f"https://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard"
|
||||||
response = self.session.get(url, headers=self.headers, timeout=15)
|
response = self.session.get(url, params={"dates": current_year, "limit":1000}, headers=self.headers, timeout=15)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
events = data.get('events', [])
|
events = data.get('events', [])
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, date
|
from datetime import date
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import ImageDraw, ImageFont
|
||||||
import numpy as np
|
|
||||||
from rgbmatrix import graphics
|
|
||||||
import pytz
|
|
||||||
from src.config_manager import ConfigManager
|
from src.config_manager import ConfigManager
|
||||||
import time
|
import time
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import time
|
|||||||
import logging
|
import logging
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
import requests
|
import requests
|
||||||
from rgbmatrix import RGBMatrix, RGBMatrixOptions
|
|
||||||
import os
|
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
|
|
||||||
# Import the API counter function from web interface
|
# Import the API counter function from web interface
|
||||||
|
|||||||