mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 13:02:59 +00:00
format gambling displays for all sports, add gambling ticker
This commit is contained in:
132
README.md
132
README.md
@@ -62,6 +62,45 @@ The system supports live, recent, and upcoming game information for multiple spo
|
||||
- Soccer
|
||||
- (Note, some of these sports seasons were not active during development and might need fine tuning when games are active)
|
||||
|
||||
### Odds Ticker Feature
|
||||
The system includes a comprehensive odds ticker that displays betting odds for upcoming sports games across multiple leagues. The ticker shows game times, team logos, spreads, money lines, and over/under totals in a scrolling format.
|
||||
|
||||
**Features:**
|
||||
- **Multi-League Support**: NFL, NBA, MLB, NCAA Football
|
||||
- **Configurable Leagues**: Choose which leagues to display
|
||||
- **Favorite Teams Filter**: Option to show only favorite teams or all games
|
||||
- **Team Logos**: Displays team logos alongside odds information
|
||||
- **Comprehensive Odds**: Shows spreads, money lines, and over/under totals
|
||||
- **Scrolling Display**: Smooth scrolling text with team logos
|
||||
- **Time Display**: Shows game times in local timezone
|
||||
|
||||
**Display Format:**
|
||||
```
|
||||
[12:00 PM] DAL -6.5 ML -200 O/U 47.5 vs NYG ML +175
|
||||
```
|
||||
|
||||
**Configuration:**
|
||||
Add the following section to your `config/config.json`:
|
||||
```json
|
||||
{
|
||||
"odds_ticker": {
|
||||
"enabled": true,
|
||||
"show_favorite_teams_only": false,
|
||||
"enabled_leagues": ["nfl", "nba", "mlb", "ncaa_fb"],
|
||||
"update_interval": 3600,
|
||||
"scroll_speed": 2,
|
||||
"scroll_delay": 0.05,
|
||||
"display_duration": 30
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Testing:**
|
||||
You can test the odds ticker functionality using:
|
||||
```bash
|
||||
python test_odds_ticker.py
|
||||
```
|
||||
|
||||
### Financial Information
|
||||
- Near real-time stock & crypto price updates
|
||||
- Stock news headlines
|
||||
@@ -299,6 +338,99 @@ The calendar display will show:
|
||||
- Event title (wrapped to fit the display)
|
||||
- Up to 3 upcoming events (configurable)
|
||||
|
||||
## Odds Ticker Configuration
|
||||
|
||||
The odds ticker displays betting odds for upcoming sports games. To configure it:
|
||||
|
||||
1. In `config/config.json`, add the following section:
|
||||
```json
|
||||
{
|
||||
"odds_ticker": {
|
||||
"enabled": true,
|
||||
"show_favorite_teams_only": false,
|
||||
"enabled_leagues": ["nfl", "nba", "mlb", "ncaa_fb"],
|
||||
"update_interval": 3600,
|
||||
"scroll_speed": 2,
|
||||
"scroll_delay": 0.05,
|
||||
"display_duration": 30
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
- **`enabled`**: Enable/disable the odds ticker (default: false)
|
||||
- **`show_favorite_teams_only`**: Show only games involving favorite teams (default: false)
|
||||
- **`enabled_leagues`**: Array of leagues to display (options: "nfl", "nba", "mlb", "ncaa_fb")
|
||||
- **`update_interval`**: How often to fetch new odds data in seconds (default: 3600)
|
||||
- **`scroll_speed`**: Pixels to scroll per update (default: 2)
|
||||
- **`scroll_delay`**: Delay between scroll updates in seconds (default: 0.05)
|
||||
- **`display_duration`**: How long to show each game in seconds (default: 30)
|
||||
|
||||
### Display Format
|
||||
|
||||
The odds ticker shows information in this format:
|
||||
```
|
||||
[12:00 PM] DAL -6.5 ML -200 O/U 47.5 vs NYG ML +175
|
||||
```
|
||||
|
||||
Where:
|
||||
- `[12:00 PM]` - Game time in local timezone
|
||||
- `DAL` - Away team abbreviation
|
||||
- `-6.5` - Spread for away team (negative = favored)
|
||||
- `ML -200` - Money line for away team
|
||||
- `O/U 47.5` - Over/under total
|
||||
- `vs` - Separator
|
||||
- `NYG` - Home team abbreviation
|
||||
- `ML +175` - Money line for home team
|
||||
|
||||
### Team Logos
|
||||
|
||||
The ticker displays team logos alongside the text:
|
||||
- Away team logo appears to the left of the text
|
||||
- Home team logo appears to the right of the text
|
||||
- Logos are automatically resized to fit the display
|
||||
|
||||
### Requirements
|
||||
|
||||
- ESPN API access for odds data
|
||||
- Team logo files in the appropriate directories:
|
||||
- `assets/sports/nfl_logos/`
|
||||
- `assets/sports/nba_logos/`
|
||||
- `assets/sports/mlb_logos/`
|
||||
- `assets/sports/ncaa_fbs_logos/`
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
**No Games Displayed:**
|
||||
1. **League Configuration**: Ensure the leagues you want are enabled in their respective config sections
|
||||
2. **Favorite Teams**: If `show_favorite_teams_only` is true, ensure you have favorite teams configured
|
||||
3. **API Access**: Verify ESPN API is accessible and returning data
|
||||
4. **Time Window**: The ticker only shows games in the next 7 days
|
||||
|
||||
**No Odds Data:**
|
||||
1. **API Timing**: Odds may not be available immediately when games are scheduled
|
||||
2. **League Support**: Not all leagues may have odds data available
|
||||
3. **API Limits**: ESPN API may have rate limits or temporary issues
|
||||
|
||||
**Performance Issues:**
|
||||
1. **Reduce scroll_speed**: Try setting it to 1 instead of 2
|
||||
2. **Increase scroll_delay**: Try 0.1 instead of 0.05
|
||||
3. **Check system resources**: Ensure the Raspberry Pi has adequate resources
|
||||
|
||||
### Testing
|
||||
|
||||
You can test the odds ticker functionality using:
|
||||
```bash
|
||||
python test_odds_ticker.py
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Initialize the odds ticker
|
||||
2. Fetch upcoming games and odds
|
||||
3. Display sample games
|
||||
4. Test the scrolling functionality
|
||||
|
||||
## Music Display Configuration
|
||||
|
||||
The Music Display module shows information about the currently playing track from either Spotify or YouTube Music (via the [YouTube Music Desktop App](https://ytmdesktop.app/) companion server).
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"hourly_forecast": 15,
|
||||
"daily_forecast": 15,
|
||||
"stock_news": 20,
|
||||
"odds_ticker": 25,
|
||||
"nhl_live": 30,
|
||||
"nhl_recent": 20,
|
||||
"nhl_upcoming": 20,
|
||||
@@ -106,6 +107,15 @@
|
||||
"max_headlines_per_symbol": 1,
|
||||
"headlines_per_rotation": 2
|
||||
},
|
||||
"odds_ticker": {
|
||||
"enabled": true,
|
||||
"show_favorite_teams_only": false,
|
||||
"enabled_leagues": ["nfl", "nba", "mlb", "ncaa_fb"],
|
||||
"update_interval": 3600,
|
||||
"scroll_speed": 2,
|
||||
"scroll_delay": 0.05,
|
||||
"display_duration": 30
|
||||
},
|
||||
"calendar": {
|
||||
"enabled": true,
|
||||
"credentials_file": "credentials.json",
|
||||
@@ -137,6 +147,8 @@
|
||||
"test_mode": false,
|
||||
"update_interval_seconds": 3600,
|
||||
"live_update_interval": 30,
|
||||
"live_odds_update_interval": 3600,
|
||||
"odds_update_interval": 3600,
|
||||
"recent_update_interval": 3600,
|
||||
"upcoming_update_interval": 3600,
|
||||
"recent_game_hours": 72,
|
||||
@@ -154,6 +166,8 @@
|
||||
"test_mode": false,
|
||||
"update_interval_seconds": 3600,
|
||||
"live_update_interval": 30,
|
||||
"live_odds_update_interval": 3600,
|
||||
"odds_update_interval": 3600,
|
||||
"past_fetch_days": 7,
|
||||
"future_fetch_days": 7,
|
||||
"favorite_teams": ["TB", "DAL"],
|
||||
@@ -170,6 +184,8 @@
|
||||
"test_mode": false,
|
||||
"update_interval_seconds": 3600,
|
||||
"live_update_interval": 30,
|
||||
"live_odds_update_interval": 3600,
|
||||
"odds_update_interval": 3600,
|
||||
"past_fetch_days": 7,
|
||||
"future_fetch_days": 7,
|
||||
"favorite_teams": ["UGA", "AUB"],
|
||||
@@ -222,6 +238,8 @@
|
||||
"test_mode": false,
|
||||
"update_interval_seconds": 3600,
|
||||
"live_update_interval": 30,
|
||||
"live_odds_update_interval": 3600,
|
||||
"odds_update_interval": 3600,
|
||||
"recent_update_interval": 3600,
|
||||
"upcoming_update_interval": 3600,
|
||||
"recent_game_hours": 48,
|
||||
|
||||
@@ -18,6 +18,7 @@ from src.display_manager import DisplayManager
|
||||
from src.config_manager import ConfigManager
|
||||
from src.stock_manager import StockManager
|
||||
from src.stock_news_manager import StockNewsManager
|
||||
from src.odds_ticker_manager import OddsTickerManager
|
||||
from src.nhl_managers import NHLLiveManager, NHLRecentManager, NHLUpcomingManager
|
||||
from src.nba_managers import NBALiveManager, NBARecentManager, NBAUpcomingManager
|
||||
from src.mlb_manager import MLBLiveManager, MLBRecentManager, MLBUpcomingManager
|
||||
@@ -54,6 +55,7 @@ class DisplayController:
|
||||
self.weather = WeatherManager(self.config, self.display_manager) if self.config.get('weather', {}).get('enabled', False) else None
|
||||
self.stocks = StockManager(self.config, self.display_manager) if self.config.get('stocks', {}).get('enabled', False) else None
|
||||
self.news = StockNewsManager(self.config, self.display_manager) if self.config.get('stock_news', {}).get('enabled', False) else None
|
||||
self.odds_ticker = OddsTickerManager(self.config, self.display_manager) if self.config.get('odds_ticker', {}).get('enabled', False) else None
|
||||
self.calendar = CalendarManager(self.display_manager, self.config) if self.config.get('calendar', {}).get('enabled', False) else None
|
||||
self.youtube = YouTubeDisplay(self.display_manager, self.config) if self.config.get('youtube', {}).get('enabled', False) else None
|
||||
self.text_display = TextDisplay(self.display_manager, self.config) if self.config.get('text_display', {}).get('enabled', False) else None
|
||||
@@ -234,6 +236,7 @@ class DisplayController:
|
||||
if self.weather: self.available_modes.extend(['weather_current', 'weather_hourly', 'weather_daily'])
|
||||
if self.stocks: self.available_modes.append('stocks')
|
||||
if self.news: self.available_modes.append('stock_news')
|
||||
if self.odds_ticker: self.available_modes.append('odds_ticker')
|
||||
if self.calendar: self.available_modes.append('calendar')
|
||||
if self.youtube: self.available_modes.append('youtube')
|
||||
if self.text_display: self.available_modes.append('text_display')
|
||||
@@ -461,6 +464,7 @@ class DisplayController:
|
||||
if self.weather: self.weather.get_weather()
|
||||
if self.stocks: self.stocks.update_stock_data()
|
||||
if self.news: self.news.update_news_data()
|
||||
if self.odds_ticker: self.odds_ticker.update()
|
||||
if self.calendar: self.calendar.update(time.time())
|
||||
if self.youtube: self.youtube.update()
|
||||
if self.text_display: self.text_display.update()
|
||||
@@ -965,13 +969,15 @@ class DisplayController:
|
||||
elif self.current_display_mode == 'stocks' and self.stocks:
|
||||
manager_to_display = self.stocks
|
||||
elif self.current_display_mode == 'stock_news' and self.news:
|
||||
manager_to_display = self.news
|
||||
manager_to_display = self.news
|
||||
elif self.current_display_mode == 'odds_ticker' and self.odds_ticker:
|
||||
manager_to_display = self.odds_ticker
|
||||
elif self.current_display_mode == 'calendar' and self.calendar:
|
||||
manager_to_display = self.calendar
|
||||
manager_to_display = self.calendar
|
||||
elif self.current_display_mode == 'youtube' and self.youtube:
|
||||
manager_to_display = self.youtube
|
||||
manager_to_display = self.youtube
|
||||
elif self.current_display_mode == 'text_display' and self.text_display:
|
||||
manager_to_display = self.text_display
|
||||
manager_to_display = self.text_display
|
||||
# Add other regular managers (NHL recent/upcoming, NBA, MLB, Soccer, NFL, NCAA FB)
|
||||
elif self.current_display_mode == 'nhl_recent' and self.nhl_recent:
|
||||
manager_to_display = self.nhl_recent
|
||||
@@ -1035,6 +1041,8 @@ class DisplayController:
|
||||
manager_to_display.display_stocks(force_clear=self.force_clear)
|
||||
elif self.current_display_mode == 'stock_news':
|
||||
manager_to_display.display_news() # Assumes internal clearing
|
||||
elif self.current_display_mode == 'odds_ticker':
|
||||
manager_to_display.display(force_clear=self.force_clear)
|
||||
elif self.current_display_mode == 'calendar':
|
||||
manager_to_display.display(force_clear=self.force_clear)
|
||||
elif self.current_display_mode == 'youtube':
|
||||
|
||||
@@ -285,20 +285,19 @@ class BaseMLBManager:
|
||||
self.display_manager.draw = draw
|
||||
self.display_manager._draw_bdf_text(status_text, status_x, status_y, color=(255, 255, 255), font=self.display_manager.calendar_font)
|
||||
|
||||
# Show spreads and over/under if available
|
||||
# Always show scores for recent/final games
|
||||
away_score = str(game_data['away_score'])
|
||||
home_score = str(game_data['home_score'])
|
||||
display_text = f"{away_score}-{home_score}"
|
||||
font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 12)
|
||||
display_width = draw.textlength(display_text, font=font)
|
||||
display_x = (width - display_width) // 2
|
||||
display_y = (height - font.size) // 2
|
||||
self._draw_text_with_outline(draw, display_text, (display_x, display_y), font, fill=(255, 255, 255))
|
||||
|
||||
# Show spreads and over/under if available (on top of scores)
|
||||
if 'odds' in game_data and game_data['odds']:
|
||||
self._draw_dynamic_odds(draw, game_data['odds'], width, height)
|
||||
|
||||
# Show score in center if no odds available
|
||||
if 'odds' not in game_data or not game_data['odds']:
|
||||
away_score = str(game_data['away_score'])
|
||||
home_score = str(game_data['home_score'])
|
||||
display_text = f"{away_score}-{home_score}"
|
||||
font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 12)
|
||||
display_width = draw.textlength(display_text, font=font)
|
||||
display_x = (width - display_width) // 2
|
||||
display_y = (height - font.size) // 2
|
||||
self._draw_text_with_outline(draw, display_text, (display_x, display_y), font, fill=(255, 255, 255))
|
||||
|
||||
# For live games, show detailed game state
|
||||
elif game_data['status'] == 'status_in_progress' or game_data.get('live', False):
|
||||
|
||||
@@ -359,6 +359,44 @@ class BaseNBAManager:
|
||||
# For non-live games, use the shared cache
|
||||
return self._fetch_shared_data(date_str)
|
||||
|
||||
def _fetch_odds(self, game: Dict) -> None:
|
||||
"""Fetch odds for a specific game if conditions are met."""
|
||||
self.logger.debug(f"Checking odds for game: {game.get('id', 'N/A')}")
|
||||
|
||||
# Ensure the API key is set in the secrets config
|
||||
if not self.config_manager.get_secret("the_odds_api_key"):
|
||||
if self._should_log('no_api_key', cooldown=3600): # Log once per hour
|
||||
self.logger.warning("Odds API key not found. Skipping odds fetch.")
|
||||
return
|
||||
|
||||
# Check if odds should be shown for this sport
|
||||
if not self.show_odds:
|
||||
self.logger.debug("Odds display is disabled for NBA.")
|
||||
return
|
||||
|
||||
# Fetch odds using OddsManager
|
||||
try:
|
||||
# Determine update interval based on game state
|
||||
is_live = game.get('is_live', False)
|
||||
update_interval = self.nba_config.get("live_odds_update_interval", 3600) if is_live \
|
||||
else self.nba_config.get("odds_update_interval", 3600)
|
||||
|
||||
odds_data = self.odds_manager.get_odds(
|
||||
sport="basketball",
|
||||
league="nba",
|
||||
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 _extract_game_details(self, game_event: Dict) -> Optional[Dict]:
|
||||
"""Extract relevant game details from ESPN API response."""
|
||||
if not game_event:
|
||||
@@ -533,6 +571,10 @@ class BaseNBAManager:
|
||||
clock_y = period_y + 10 # Position below period
|
||||
draw.text((clock_x, clock_y), clock, font=self.fonts['time'], fill=(255, 255, 255))
|
||||
|
||||
# Draw odds if available
|
||||
if 'odds' in game and game['odds']:
|
||||
self._draw_dynamic_odds(draw, game['odds'], self.display_width, self.display_height)
|
||||
|
||||
# Display the image
|
||||
self.display_manager.image.paste(main_img, (0, 0))
|
||||
self.display_manager.update_display()
|
||||
@@ -553,152 +595,153 @@ class BaseNBAManager:
|
||||
|
||||
self._draw_scorebug_layout(self.current_game, force_clear)
|
||||
|
||||
def _draw_dynamic_odds(self, draw: ImageDraw.Draw, odds: Dict[str, Any], width: int, height: int) -> None:
|
||||
"""Draw odds with dynamic positioning - only show negative spread and position O/U based on favored team."""
|
||||
home_team_odds = odds.get('home_team_odds', {})
|
||||
away_team_odds = odds.get('away_team_odds', {})
|
||||
home_spread = home_team_odds.get('spread_odds')
|
||||
away_spread = away_team_odds.get('spread_odds')
|
||||
|
||||
# Get top-level spread as fallback
|
||||
top_level_spread = odds.get('spread')
|
||||
|
||||
# If we have a top-level spread and the individual spreads are None or 0, use the top-level
|
||||
if top_level_spread is not None:
|
||||
if home_spread is None or home_spread == 0.0:
|
||||
home_spread = top_level_spread
|
||||
if away_spread is None:
|
||||
away_spread = -top_level_spread
|
||||
|
||||
# Determine which team is favored (has negative spread)
|
||||
home_favored = home_spread is not None and home_spread < 0
|
||||
away_favored = away_spread is not None and away_spread < 0
|
||||
|
||||
# Only show the negative spread (favored team)
|
||||
favored_spread = None
|
||||
favored_side = None
|
||||
|
||||
if home_favored:
|
||||
favored_spread = home_spread
|
||||
favored_side = 'home'
|
||||
self.logger.debug(f"Home team favored with spread: {favored_spread}")
|
||||
elif away_favored:
|
||||
favored_spread = away_spread
|
||||
favored_side = 'away'
|
||||
self.logger.debug(f"Away team favored with spread: {favored_spread}")
|
||||
else:
|
||||
self.logger.debug("No clear favorite - spreads: home={home_spread}, away={away_spread}")
|
||||
|
||||
# Show the negative spread on the appropriate side
|
||||
if favored_spread is not None:
|
||||
spread_text = str(favored_spread)
|
||||
font = self.fonts['detail'] # Use detail font for odds
|
||||
|
||||
if favored_side == 'home':
|
||||
# Home team is favored, show spread on right side
|
||||
spread_width = draw.textlength(spread_text, font=font)
|
||||
spread_x = width - spread_width - 2 # Top right
|
||||
spread_y = 2
|
||||
self._draw_text_with_outline(draw, spread_text, (spread_x, spread_y), font, fill=(0, 255, 0))
|
||||
self.logger.debug(f"Showing home spread '{spread_text}' on right side")
|
||||
else:
|
||||
# Away team is favored, show spread on left side
|
||||
spread_x = 2 # Top left
|
||||
spread_y = 2
|
||||
self._draw_text_with_outline(draw, spread_text, (spread_x, spread_y), font, fill=(0, 255, 0))
|
||||
self.logger.debug(f"Showing away spread '{spread_text}' on left side")
|
||||
|
||||
# Show over/under on the opposite side of the favored team
|
||||
over_under = odds.get('over_under')
|
||||
if over_under is not None:
|
||||
ou_text = f"O/U: {over_under}"
|
||||
font = self.fonts['detail'] # Use detail font for odds
|
||||
ou_width = draw.textlength(ou_text, font=font)
|
||||
|
||||
if favored_side == 'home':
|
||||
# Home team is favored, show O/U on left side (opposite of spread)
|
||||
ou_x = 2 # Top left
|
||||
ou_y = 2
|
||||
self.logger.debug(f"Showing O/U '{ou_text}' on left side (home favored)")
|
||||
elif favored_side == 'away':
|
||||
# Away team is favored, show O/U on right side (opposite of spread)
|
||||
ou_x = width - ou_width - 2 # Top right
|
||||
ou_y = 2
|
||||
self.logger.debug(f"Showing O/U '{ou_text}' on right side (away favored)")
|
||||
else:
|
||||
# No clear favorite, show O/U in center
|
||||
ou_x = (width - ou_width) // 2
|
||||
ou_y = 2
|
||||
self.logger.debug(f"Showing O/U '{ou_text}' in center (no clear favorite)")
|
||||
|
||||
self._draw_text_with_outline(draw, ou_text, (ou_x, ou_y), font, fill=(0, 255, 0))
|
||||
|
||||
def _draw_text_with_outline(self, draw, text, position, font, fill=(255, 255, 255), outline_color=(0, 0, 0)):
|
||||
"""Helper to draw text with an outline."""
|
||||
draw.text(position, text, font=font, fill=outline_color)
|
||||
draw.text(position, text, font=font, fill=fill)
|
||||
|
||||
|
||||
class NBALiveManager(BaseNBAManager):
|
||||
"""Manager for live NBA games."""
|
||||
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager):
|
||||
super().__init__(config, display_manager)
|
||||
self.update_interval = self.nba_config.get("live_update_interval", 15) # 15 seconds for live games
|
||||
self.no_data_interval = 300 # 5 minutes when no live games
|
||||
self.update_interval = self.nba_config.get("live_update_interval", 30)
|
||||
self.no_data_interval = 300
|
||||
self.last_update = 0
|
||||
self.logger.info("Initialized NBA 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.nba_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.live_games = []
|
||||
self.current_game_index = 0
|
||||
self.last_game_switch = 0
|
||||
self.game_display_duration = self.nba_config.get("live_game_duration", 20)
|
||||
self.last_display_update = 0
|
||||
self.last_log_time = 0
|
||||
self.log_interval = 300 # Only log status every 5 minutes
|
||||
self.has_favorite_team_game = False # Track if we have any favorite team games
|
||||
|
||||
# Initialize with test game only if test mode is enabled
|
||||
if self.test_mode:
|
||||
self.current_game = {
|
||||
"home_abbr": "LAL",
|
||||
"away_abbr": "BOS",
|
||||
"home_score": "85",
|
||||
"away_score": "82",
|
||||
"period": 3,
|
||||
"clock": "12:34",
|
||||
"home_logo_path": os.path.join(self.logo_dir, "LAL.png"),
|
||||
"away_logo_path": os.path.join(self.logo_dir, "BOS.png"),
|
||||
"game_time": "7:30 PM",
|
||||
"game_date": "Apr 17"
|
||||
}
|
||||
self.live_games = [self.current_game]
|
||||
self.logger.info("[NBA] Initialized NBALiveManager with test game: LAL vs BOS")
|
||||
else:
|
||||
self.logger.info("[NBA] Initialized NBALiveManager in live mode")
|
||||
self.log_interval = 300
|
||||
|
||||
def update(self):
|
||||
"""Update live game data."""
|
||||
if not self.is_enabled: return
|
||||
current_time = time.time()
|
||||
|
||||
# Determine update interval based on whether we have favorite team games
|
||||
if self.has_favorite_team_game:
|
||||
interval = self.update_interval # 15 seconds for live favorite team games
|
||||
else:
|
||||
interval = self.no_data_interval # 5 minutes when no favorite team games
|
||||
|
||||
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 = 11
|
||||
if self.current_game["period"] < 4:
|
||||
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 = []
|
||||
has_favorite_team = False
|
||||
for event in data["events"]:
|
||||
details = self._extract_game_details(event)
|
||||
if details and details["is_live"]:
|
||||
if not self.favorite_teams or (
|
||||
details["home_abbr"] in self.favorite_teams or
|
||||
details["away_abbr"] in self.favorite_teams
|
||||
):
|
||||
new_live_games.append(details)
|
||||
if self.favorite_teams and (
|
||||
details["home_abbr"] in self.favorite_teams or
|
||||
details["away_abbr"] in self.favorite_teams
|
||||
):
|
||||
has_favorite_team = True
|
||||
|
||||
# Update favorite team game status
|
||||
self.has_favorite_team_game = has_favorite_team
|
||||
|
||||
# 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 or # Log if we had no games before
|
||||
has_favorite_team != self.has_favorite_team_game # Log if favorite team status changed
|
||||
)
|
||||
|
||||
if should_log:
|
||||
if new_live_games:
|
||||
self.logger.info(f"[NBA] Found {len(new_live_games)} live games")
|
||||
for game in new_live_games:
|
||||
self.logger.info(f"[NBA] Live game: {game['away_abbr']} vs {game['home_abbr']} - Q{game['period']}, {game['clock']}")
|
||||
if has_favorite_team:
|
||||
self.logger.info("[NBA] Found live game(s) for favorite team(s)")
|
||||
else:
|
||||
self.logger.info("[NBA] No live games found")
|
||||
self.last_log_time = current_time
|
||||
|
||||
if new_live_games:
|
||||
# Update the current game with the latest data
|
||||
for new_game in new_live_games:
|
||||
if self.current_game and (
|
||||
(new_game["home_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
|
||||
|
||||
# Only update display if we have new data and enough time has passed
|
||||
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
|
||||
self.has_favorite_team_game = False
|
||||
|
||||
def display(self, force_clear: bool = False):
|
||||
# Fetch live game data
|
||||
data = self._fetch_data()
|
||||
new_live_games = []
|
||||
if data and "events" in data:
|
||||
for event in data["events"]:
|
||||
details = self._extract_game_details(event)
|
||||
if details and details["is_live"]:
|
||||
if not self.favorite_teams or (
|
||||
details["home_abbr"] in self.favorite_teams or
|
||||
details["away_abbr"] in self.favorite_teams
|
||||
):
|
||||
# Fetch odds if enabled
|
||||
if self.show_odds:
|
||||
self._fetch_odds(details)
|
||||
new_live_games.append(details)
|
||||
|
||||
# Update game list and current game
|
||||
if new_live_games:
|
||||
self.live_games = new_live_games
|
||||
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] if self.live_games else None
|
||||
self.last_game_switch = current_time
|
||||
else:
|
||||
# Update current game with fresh data
|
||||
self.current_game = new_live_games[self.current_game_index]
|
||||
else:
|
||||
self.live_games = []
|
||||
self.current_game = None
|
||||
|
||||
def display(self, force_clear: bool = False) -> None:
|
||||
"""Display live game information."""
|
||||
if not self.current_game:
|
||||
return
|
||||
super().display(force_clear) # Call parent class's display method
|
||||
super().display(force_clear)
|
||||
|
||||
|
||||
class NBARecentManager(BaseNBAManager):
|
||||
"""Manager for recently completed NBA games."""
|
||||
@@ -708,94 +751,68 @@ class NBARecentManager(BaseNBAManager):
|
||||
self.current_game_index = 0
|
||||
self.last_update = 0
|
||||
self.update_interval = 3600 # 1 hour for recent games
|
||||
self.recent_hours = self.nba_config.get("recent_game_hours", 48)
|
||||
self.last_game_switch = 0
|
||||
self.game_display_duration = 15 # Display each game for 15 seconds
|
||||
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.logger.info(f"Initialized NBARecentManager with {len(self.favorite_teams)} favorite teams")
|
||||
|
||||
|
||||
def update(self):
|
||||
"""Update recent games data."""
|
||||
current_time = time.time()
|
||||
if current_time - self.last_update < self.update_interval:
|
||||
return
|
||||
|
||||
|
||||
try:
|
||||
# Fetch data from ESPN API
|
||||
data = self._fetch_data()
|
||||
if not data or 'events' not in data:
|
||||
self.logger.warning("[NBA] No events found in ESPN API response")
|
||||
return
|
||||
|
||||
|
||||
events = data['events']
|
||||
|
||||
# Process games
|
||||
new_recent_games = []
|
||||
for event in events:
|
||||
game = self._extract_game_details(event)
|
||||
# Filter for recent games: must be final and within the time window
|
||||
if game and game['is_final'] and game['is_within_window']:
|
||||
# Fetch odds if enabled
|
||||
if self.show_odds:
|
||||
self._fetch_odds(game)
|
||||
new_recent_games.append(game)
|
||||
|
||||
|
||||
# Filter for favorite teams
|
||||
new_team_games = [game for game in new_recent_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_team_games) != len(self.recent_games) or
|
||||
not self.recent_games # Log if we had no games before
|
||||
)
|
||||
|
||||
if should_log:
|
||||
if new_team_games:
|
||||
self.logger.info(f"[NBA] Found {len(new_team_games)} recent games for favorite teams")
|
||||
else:
|
||||
self.logger.info("[NBA] No recent games found for favorite teams")
|
||||
self.last_log_time = current_time
|
||||
|
||||
self.recent_games = new_team_games
|
||||
if self.favorite_teams:
|
||||
team_games = [game for game in new_recent_games
|
||||
if game['home_abbr'] in self.favorite_teams or
|
||||
game['away_abbr'] in self.favorite_teams]
|
||||
else:
|
||||
team_games = new_recent_games
|
||||
|
||||
self.recent_games = team_games
|
||||
if self.recent_games:
|
||||
self.current_game = self.recent_games[0]
|
||||
self.last_update = current_time
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"[NBA] Error updating recent games: {e}", exc_info=True)
|
||||
|
||||
def display(self, force_clear=False):
|
||||
"""Display recent games."""
|
||||
if not self.recent_games:
|
||||
current_time = time.time()
|
||||
if current_time - self.last_warning_time > self.warning_cooldown:
|
||||
self.logger.info("[NBA] No recent games to display")
|
||||
self.last_warning_time = current_time
|
||||
return # Skip display update entirely
|
||||
|
||||
return
|
||||
|
||||
try:
|
||||
current_time = time.time()
|
||||
|
||||
|
||||
# Check if it's time to switch games
|
||||
if current_time - self.last_game_switch >= self.game_display_duration:
|
||||
# Move to next game
|
||||
self.current_game_index = (self.current_game_index + 1) % len(self.recent_games)
|
||||
self.current_game = self.recent_games[self.current_game_index]
|
||||
self.last_game_switch = current_time
|
||||
force_clear = True # Force clear when switching games
|
||||
|
||||
force_clear = True
|
||||
|
||||
# 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"[NBA] Error displaying recent game: {e}", exc_info=True)
|
||||
|
||||
|
||||
class NBAUpcomingManager(BaseNBAManager):
|
||||
"""Manager for upcoming NBA games."""
|
||||
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager):
|
||||
@@ -804,86 +821,55 @@ class NBAUpcomingManager(BaseNBAManager):
|
||||
self.current_game_index = 0
|
||||
self.last_update = 0
|
||||
self.update_interval = 3600 # 1 hour for upcoming games
|
||||
self.last_warning_time = 0
|
||||
self.warning_cooldown = 300 # Only show warning every 5 minutes
|
||||
self.logger.info(f"Initialized NBAUpcomingManager with {len(self.favorite_teams)} favorite teams")
|
||||
|
||||
|
||||
def update(self):
|
||||
"""Update upcoming games data."""
|
||||
current_time = time.time()
|
||||
if current_time - self.last_update < self.update_interval:
|
||||
return
|
||||
|
||||
|
||||
try:
|
||||
# Fetch data from ESPN API
|
||||
data = self._fetch_data()
|
||||
if not data or 'events' not in data:
|
||||
if current_time - self.last_warning_time > self.warning_cooldown:
|
||||
self.logger.warning("[NBA] No events found in ESPN API response")
|
||||
self.last_warning_time = current_time
|
||||
self.games_list = []
|
||||
self.current_game = None
|
||||
self.last_update = current_time
|
||||
return
|
||||
|
||||
|
||||
events = data['events']
|
||||
if self._should_log("fetch_success", 300):
|
||||
self.logger.info(f"[NBA] Successfully fetched {len(events)} events from ESPN API")
|
||||
|
||||
# Process games
|
||||
self.upcoming_games = []
|
||||
for event in events:
|
||||
game = self._extract_game_details(event)
|
||||
if game and game['is_upcoming']: # Only check is_upcoming, not is_within_window
|
||||
if game and game['is_upcoming']:
|
||||
# Fetch odds if enabled
|
||||
if self.show_odds:
|
||||
self._fetch_odds(game)
|
||||
self.upcoming_games.append(game)
|
||||
self.logger.debug(f"Processing upcoming game: {game['away_abbr']} vs {game['home_abbr']}")
|
||||
|
||||
|
||||
# Filter for favorite teams
|
||||
team_games = [game for game in self.upcoming_games
|
||||
if game['home_abbr'] in self.favorite_teams or
|
||||
game['away_abbr'] in self.favorite_teams]
|
||||
|
||||
if self._should_log("team_games", 300):
|
||||
self.logger.info(f"[NBA] Found {len(team_games)} upcoming games for favorite teams")
|
||||
|
||||
if not team_games:
|
||||
if current_time - self.last_warning_time > self.warning_cooldown:
|
||||
self.logger.info("[NBA] No upcoming games found for favorite teams")
|
||||
self.last_warning_time = current_time
|
||||
self.games_list = []
|
||||
self.current_game = None
|
||||
self.last_update = current_time
|
||||
return
|
||||
|
||||
self.games_list = team_games
|
||||
self.current_game = team_games[0]
|
||||
if self.favorite_teams:
|
||||
team_games = [game for game in self.upcoming_games
|
||||
if game['home_abbr'] in self.favorite_teams or
|
||||
game['away_abbr'] in self.favorite_teams]
|
||||
else:
|
||||
team_games = self.upcoming_games
|
||||
|
||||
if team_games:
|
||||
self.current_game = team_games[0]
|
||||
self.last_update = current_time
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"[NBA] Error updating upcoming games: {e}", exc_info=True)
|
||||
self.games_list = []
|
||||
self.current_game = None
|
||||
self.last_update = current_time
|
||||
|
||||
def display(self, force_clear=False):
|
||||
"""Display upcoming games."""
|
||||
if not self.games_list:
|
||||
current_time = time.time()
|
||||
if current_time - self.last_warning_time > self.warning_cooldown:
|
||||
self.logger.info("[NBA] No upcoming games to display")
|
||||
self.last_warning_time = current_time
|
||||
return # Skip display update entirely
|
||||
|
||||
if not self.upcoming_games:
|
||||
return
|
||||
|
||||
try:
|
||||
# Draw the scorebug layout
|
||||
self._draw_scorebug_layout(self.current_game, force_clear)
|
||||
|
||||
# Update display
|
||||
self.display_manager.update_display()
|
||||
|
||||
|
||||
# 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.current_game_index = (self.current_game_index + 1) % len(self.upcoming_games)
|
||||
self.current_game = self.upcoming_games[self.current_game_index]
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"[NBA] Error displaying upcoming game: {e}", exc_info=True)
|
||||
@@ -279,6 +279,86 @@ class BaseNCAAFBManager: # Renamed class
|
||||
fonts['detail'] = ImageFont.load_default()
|
||||
return fonts
|
||||
|
||||
def _draw_dynamic_odds(self, draw: ImageDraw.Draw, odds: Dict[str, Any], width: int, height: int) -> None:
|
||||
"""Draw odds with dynamic positioning - only show negative spread and position O/U based on favored team."""
|
||||
home_team_odds = odds.get('home_team_odds', {})
|
||||
away_team_odds = odds.get('away_team_odds', {})
|
||||
home_spread = home_team_odds.get('spread_odds')
|
||||
away_spread = away_team_odds.get('spread_odds')
|
||||
|
||||
# Get top-level spread as fallback
|
||||
top_level_spread = odds.get('spread')
|
||||
|
||||
# If we have a top-level spread and the individual spreads are None or 0, use the top-level
|
||||
if top_level_spread is not None:
|
||||
if home_spread is None or home_spread == 0.0:
|
||||
home_spread = top_level_spread
|
||||
if away_spread is None:
|
||||
away_spread = -top_level_spread
|
||||
|
||||
# Determine which team is favored (has negative spread)
|
||||
home_favored = home_spread is not None and home_spread < 0
|
||||
away_favored = away_spread is not None and away_spread < 0
|
||||
|
||||
# Only show the negative spread (favored team)
|
||||
favored_spread = None
|
||||
favored_side = None
|
||||
|
||||
if home_favored:
|
||||
favored_spread = home_spread
|
||||
favored_side = 'home'
|
||||
self.logger.debug(f"Home team favored with spread: {favored_spread}")
|
||||
elif away_favored:
|
||||
favored_spread = away_spread
|
||||
favored_side = 'away'
|
||||
self.logger.debug(f"Away team favored with spread: {favored_spread}")
|
||||
else:
|
||||
self.logger.debug("No clear favorite - spreads: home={home_spread}, away={away_spread}")
|
||||
|
||||
# Show the negative spread on the appropriate side
|
||||
if favored_spread is not None:
|
||||
spread_text = str(favored_spread)
|
||||
font = self.fonts['detail'] # Use detail font for odds
|
||||
|
||||
if favored_side == 'home':
|
||||
# Home team is favored, show spread on right side
|
||||
spread_width = draw.textlength(spread_text, font=font)
|
||||
spread_x = width - spread_width - 2 # Top right
|
||||
spread_y = 2
|
||||
self._draw_text_with_outline(draw, spread_text, (spread_x, spread_y), font, fill=(0, 255, 0))
|
||||
self.logger.debug(f"Showing home spread '{spread_text}' on right side")
|
||||
else:
|
||||
# Away team is favored, show spread on left side
|
||||
spread_x = 2 # Top left
|
||||
spread_y = 2
|
||||
self._draw_text_with_outline(draw, spread_text, (spread_x, spread_y), font, fill=(0, 255, 0))
|
||||
self.logger.debug(f"Showing away spread '{spread_text}' on left side")
|
||||
|
||||
# Show over/under on the opposite side of the favored team
|
||||
over_under = odds.get('over_under')
|
||||
if over_under is not None:
|
||||
ou_text = f"O/U: {over_under}"
|
||||
font = self.fonts['detail'] # Use detail font for odds
|
||||
ou_width = draw.textlength(ou_text, font=font)
|
||||
|
||||
if favored_side == 'home':
|
||||
# Home team is favored, show O/U on left side (opposite of spread)
|
||||
ou_x = 2 # Top left
|
||||
ou_y = 2
|
||||
self.logger.debug(f"Showing O/U '{ou_text}' on left side (home favored)")
|
||||
elif favored_side == 'away':
|
||||
# Away team is favored, show O/U on right side (opposite of spread)
|
||||
ou_x = width - ou_width - 2 # Top right
|
||||
ou_y = 2
|
||||
self.logger.debug(f"Showing O/U '{ou_text}' on right side (away favored)")
|
||||
else:
|
||||
# No clear favorite, show O/U in center
|
||||
ou_x = (width - ou_width) // 2
|
||||
ou_y = 2
|
||||
self.logger.debug(f"Showing O/U '{ou_text}' in center (no clear favorite)")
|
||||
|
||||
self._draw_text_with_outline(draw, ou_text, (ou_x, ou_y), font, fill=(0, 255, 0))
|
||||
|
||||
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."""
|
||||
x, y = position
|
||||
@@ -786,6 +866,10 @@ class NCAAFBLiveManager(BaseNCAAFBManager): # Renamed class
|
||||
color = (255, 255, 255) if i < home_timeouts_remaining else (80, 80, 80) # White if available, gray if used
|
||||
draw_overlay.rectangle([to_x, timeout_y, to_x + timeout_bar_width, timeout_y + timeout_bar_height], fill=color, outline=(0,0,0))
|
||||
|
||||
# Draw odds if available
|
||||
if 'odds' in game and game['odds']:
|
||||
self._draw_dynamic_odds(draw_overlay, game['odds'], self.display_width, self.display_height)
|
||||
|
||||
# Composite the text overlay onto the main image
|
||||
main_img = Image.alpha_composite(main_img, overlay)
|
||||
main_img = main_img.convert('RGB') # Convert for display
|
||||
@@ -932,6 +1016,10 @@ class NCAAFBRecentManager(BaseNCAAFBManager): # Renamed class
|
||||
status_y = 1
|
||||
self._draw_text_with_outline(draw_overlay, status_text, (status_x, status_y), self.fonts['time'])
|
||||
|
||||
# Draw odds if available
|
||||
if 'odds' in game and game['odds']:
|
||||
self._draw_dynamic_odds(draw_overlay, game['odds'], self.display_width, self.display_height)
|
||||
|
||||
# Composite and display
|
||||
main_img = Image.alpha_composite(main_img, overlay)
|
||||
main_img = main_img.convert('RGB')
|
||||
@@ -1122,6 +1210,10 @@ class NCAAFBUpcomingManager(BaseNCAAFBManager): # Renamed class
|
||||
time_y = date_y + 9 # Place time below date
|
||||
self._draw_text_with_outline(draw_overlay, game_time, (time_x, time_y), self.fonts['time'])
|
||||
|
||||
# Draw odds if available
|
||||
if 'odds' in game and game['odds']:
|
||||
self._draw_dynamic_odds(draw_overlay, game['odds'], self.display_width, self.display_height)
|
||||
|
||||
# Composite and display
|
||||
main_img = Image.alpha_composite(main_img, overlay)
|
||||
main_img = main_img.convert('RGB')
|
||||
|
||||
@@ -279,6 +279,86 @@ class BaseNFLManager: # Renamed class
|
||||
fonts['detail'] = ImageFont.load_default()
|
||||
return fonts
|
||||
|
||||
def _draw_dynamic_odds(self, draw: ImageDraw.Draw, odds: Dict[str, Any], width: int, height: int) -> None:
|
||||
"""Draw odds with dynamic positioning - only show negative spread and position O/U based on favored team."""
|
||||
home_team_odds = odds.get('home_team_odds', {})
|
||||
away_team_odds = odds.get('away_team_odds', {})
|
||||
home_spread = home_team_odds.get('spread_odds')
|
||||
away_spread = away_team_odds.get('spread_odds')
|
||||
|
||||
# Get top-level spread as fallback
|
||||
top_level_spread = odds.get('spread')
|
||||
|
||||
# If we have a top-level spread and the individual spreads are None or 0, use the top-level
|
||||
if top_level_spread is not None:
|
||||
if home_spread is None or home_spread == 0.0:
|
||||
home_spread = top_level_spread
|
||||
if away_spread is None:
|
||||
away_spread = -top_level_spread
|
||||
|
||||
# Determine which team is favored (has negative spread)
|
||||
home_favored = home_spread is not None and home_spread < 0
|
||||
away_favored = away_spread is not None and away_spread < 0
|
||||
|
||||
# Only show the negative spread (favored team)
|
||||
favored_spread = None
|
||||
favored_side = None
|
||||
|
||||
if home_favored:
|
||||
favored_spread = home_spread
|
||||
favored_side = 'home'
|
||||
self.logger.debug(f"Home team favored with spread: {favored_spread}")
|
||||
elif away_favored:
|
||||
favored_spread = away_spread
|
||||
favored_side = 'away'
|
||||
self.logger.debug(f"Away team favored with spread: {favored_spread}")
|
||||
else:
|
||||
self.logger.debug("No clear favorite - spreads: home={home_spread}, away={away_spread}")
|
||||
|
||||
# Show the negative spread on the appropriate side
|
||||
if favored_spread is not None:
|
||||
spread_text = str(favored_spread)
|
||||
font = self.fonts['detail'] # Use detail font for odds
|
||||
|
||||
if favored_side == 'home':
|
||||
# Home team is favored, show spread on right side
|
||||
spread_width = draw.textlength(spread_text, font=font)
|
||||
spread_x = width - spread_width - 2 # Top right
|
||||
spread_y = 2
|
||||
self._draw_text_with_outline(draw, spread_text, (spread_x, spread_y), font, fill=(0, 255, 0))
|
||||
self.logger.debug(f"Showing home spread '{spread_text}' on right side")
|
||||
else:
|
||||
# Away team is favored, show spread on left side
|
||||
spread_x = 2 # Top left
|
||||
spread_y = 2
|
||||
self._draw_text_with_outline(draw, spread_text, (spread_x, spread_y), font, fill=(0, 255, 0))
|
||||
self.logger.debug(f"Showing away spread '{spread_text}' on left side")
|
||||
|
||||
# Show over/under on the opposite side of the favored team
|
||||
over_under = odds.get('over_under')
|
||||
if over_under is not None:
|
||||
ou_text = f"O/U: {over_under}"
|
||||
font = self.fonts['detail'] # Use detail font for odds
|
||||
ou_width = draw.textlength(ou_text, font=font)
|
||||
|
||||
if favored_side == 'home':
|
||||
# Home team is favored, show O/U on left side (opposite of spread)
|
||||
ou_x = 2 # Top left
|
||||
ou_y = 2
|
||||
self.logger.debug(f"Showing O/U '{ou_text}' on left side (home favored)")
|
||||
elif favored_side == 'away':
|
||||
# Away team is favored, show O/U on right side (opposite of spread)
|
||||
ou_x = width - ou_width - 2 # Top right
|
||||
ou_y = 2
|
||||
self.logger.debug(f"Showing O/U '{ou_text}' on right side (away favored)")
|
||||
else:
|
||||
# No clear favorite, show O/U in center
|
||||
ou_x = (width - ou_width) // 2
|
||||
ou_y = 2
|
||||
self.logger.debug(f"Showing O/U '{ou_text}' in center (no clear favorite)")
|
||||
|
||||
self._draw_text_with_outline(draw, ou_text, (ou_x, ou_y), font, fill=(0, 255, 0))
|
||||
|
||||
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."""
|
||||
x, y = position
|
||||
@@ -580,6 +660,9 @@ class NFLLiveManager(BaseNFLManager): # Renamed class
|
||||
details["home_abbr"] in self.favorite_teams or
|
||||
details["away_abbr"] in self.favorite_teams
|
||||
):
|
||||
# Fetch odds if enabled
|
||||
if self.show_odds:
|
||||
self._fetch_odds(details)
|
||||
new_live_games.append(details)
|
||||
|
||||
# Log changes or periodically
|
||||
@@ -769,6 +852,10 @@ class NFLLiveManager(BaseNFLManager): # Renamed class
|
||||
color = (255, 255, 255) if i < home_timeouts_remaining else (80, 80, 80) # White if available, gray if used
|
||||
draw_overlay.rectangle([to_x, timeout_y, to_x + timeout_bar_width, timeout_y + timeout_bar_height], fill=color, outline=(0,0,0))
|
||||
|
||||
# Draw odds if available
|
||||
if 'odds' in game and game['odds']:
|
||||
self._draw_dynamic_odds(draw_overlay, game['odds'], self.display_width, self.display_height)
|
||||
|
||||
# Composite the text overlay onto the main image
|
||||
main_img = Image.alpha_composite(main_img, overlay)
|
||||
main_img = main_img.convert('RGB') # Convert for display
|
||||
@@ -820,6 +907,9 @@ class NFLRecentManager(BaseNFLManager): # Renamed class
|
||||
game = self._extract_game_details(event)
|
||||
# Filter criteria: must be final, within time window
|
||||
if game and game['is_final'] and game.get('is_within_window', True): # Assume within window if key missing
|
||||
# Fetch odds if enabled
|
||||
if self.show_odds:
|
||||
self._fetch_odds(game)
|
||||
processed_games.append(game)
|
||||
|
||||
# Filter for favorite teams
|
||||
@@ -915,6 +1005,10 @@ class NFLRecentManager(BaseNFLManager): # Renamed class
|
||||
status_y = 1
|
||||
self._draw_text_with_outline(draw_overlay, status_text, (status_x, status_y), self.fonts['time'])
|
||||
|
||||
# Draw odds if available
|
||||
if 'odds' in game and game['odds']:
|
||||
self._draw_dynamic_odds(draw_overlay, game['odds'], self.display_width, self.display_height)
|
||||
|
||||
# Composite and display
|
||||
main_img = Image.alpha_composite(main_img, overlay)
|
||||
main_img = main_img.convert('RGB')
|
||||
@@ -992,6 +1086,9 @@ class NFLUpcomingManager(BaseNFLManager): # Renamed class
|
||||
game = self._extract_game_details(event)
|
||||
# Filter criteria: must be upcoming ('pre' state) and within time window
|
||||
if game and game['is_upcoming'] and game.get('is_within_window', True): # Assume within window if key missing
|
||||
# Fetch odds if enabled
|
||||
if self.show_odds:
|
||||
self._fetch_odds(game)
|
||||
processed_games.append(game)
|
||||
|
||||
# Filter for favorite teams
|
||||
@@ -1105,6 +1202,10 @@ class NFLUpcomingManager(BaseNFLManager): # Renamed class
|
||||
time_y = date_y + 9 # Place time below date
|
||||
self._draw_text_with_outline(draw_overlay, game_time, (time_x, time_y), self.fonts['time'])
|
||||
|
||||
# Draw odds if available
|
||||
if 'odds' in game and game['odds']:
|
||||
self._draw_dynamic_odds(draw_overlay, game['odds'], self.display_width, self.display_height)
|
||||
|
||||
# Composite and display
|
||||
main_img = Image.alpha_composite(main_img, overlay)
|
||||
main_img = main_img.convert('RGB')
|
||||
|
||||
406
src/odds_ticker_manager.py
Normal file
406
src/odds_ticker_manager.py
Normal file
@@ -0,0 +1,406 @@
|
||||
import time
|
||||
import logging
|
||||
import requests
|
||||
import json
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import os
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import pytz
|
||||
from src.display_manager import DisplayManager
|
||||
from src.cache_manager import CacheManager
|
||||
from src.config_manager import ConfigManager
|
||||
from src.odds_manager import OddsManager
|
||||
|
||||
# Get logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class OddsTickerManager:
|
||||
"""Manager for displaying scrolling odds ticker for multiple sports leagues."""
|
||||
|
||||
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager):
|
||||
self.config = config
|
||||
self.display_manager = display_manager
|
||||
self.odds_ticker_config = config.get('odds_ticker', {})
|
||||
self.is_enabled = self.odds_ticker_config.get('enabled', False)
|
||||
self.show_favorite_teams_only = self.odds_ticker_config.get('show_favorite_teams_only', False)
|
||||
self.enabled_leagues = self.odds_ticker_config.get('enabled_leagues', ['nfl', 'nba', 'mlb'])
|
||||
self.update_interval = self.odds_ticker_config.get('update_interval', 3600)
|
||||
self.scroll_speed = self.odds_ticker_config.get('scroll_speed', 2)
|
||||
self.scroll_delay = self.odds_ticker_config.get('scroll_delay', 0.05)
|
||||
self.display_duration = self.odds_ticker_config.get('display_duration', 30)
|
||||
|
||||
# Initialize managers
|
||||
self.cache_manager = CacheManager()
|
||||
self.odds_manager = OddsManager(self.cache_manager, ConfigManager())
|
||||
|
||||
# State variables
|
||||
self.last_update = 0
|
||||
self.current_position = 0
|
||||
self.last_scroll_time = 0
|
||||
self.games_data = []
|
||||
self.current_game_index = 0
|
||||
self.current_image = None
|
||||
|
||||
# Font setup
|
||||
self.fonts = self._load_fonts()
|
||||
|
||||
# League configurations
|
||||
self.league_configs = {
|
||||
'nfl': {
|
||||
'sport': 'football',
|
||||
'league': 'nfl',
|
||||
'logo_dir': 'assets/sports/nfl_logos',
|
||||
'favorite_teams': config.get('nfl_scoreboard', {}).get('favorite_teams', []),
|
||||
'enabled': config.get('nfl_scoreboard', {}).get('enabled', False)
|
||||
},
|
||||
'nba': {
|
||||
'sport': 'basketball',
|
||||
'league': 'nba',
|
||||
'logo_dir': 'assets/sports/nba_logos',
|
||||
'favorite_teams': config.get('nba_scoreboard', {}).get('favorite_teams', []),
|
||||
'enabled': config.get('nba_scoreboard', {}).get('enabled', False)
|
||||
},
|
||||
'mlb': {
|
||||
'sport': 'baseball',
|
||||
'league': 'mlb',
|
||||
'logo_dir': 'assets/sports/mlb_logos',
|
||||
'favorite_teams': config.get('mlb', {}).get('favorite_teams', []),
|
||||
'enabled': config.get('mlb', {}).get('enabled', False)
|
||||
},
|
||||
'ncaa_fb': {
|
||||
'sport': 'football',
|
||||
'league': 'college-football',
|
||||
'logo_dir': 'assets/sports/ncaa_fbs_logos',
|
||||
'favorite_teams': config.get('ncaa_fb_scoreboard', {}).get('favorite_teams', []),
|
||||
'enabled': config.get('ncaa_fb_scoreboard', {}).get('enabled', False)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(f"OddsTickerManager initialized with enabled leagues: {self.enabled_leagues}")
|
||||
logger.info(f"Show favorite teams only: {self.show_favorite_teams_only}")
|
||||
|
||||
def _load_fonts(self) -> Dict[str, ImageFont.FreeTypeFont]:
|
||||
"""Load fonts for the ticker display."""
|
||||
try:
|
||||
return {
|
||||
'small': ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 6),
|
||||
'medium': ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8),
|
||||
'large': ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 10)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading fonts: {e}")
|
||||
return {
|
||||
'small': ImageFont.load_default(),
|
||||
'medium': ImageFont.load_default(),
|
||||
'large': ImageFont.load_default()
|
||||
}
|
||||
|
||||
def _get_team_logo(self, team_abbr: str, logo_dir: str) -> Optional[Image.Image]:
|
||||
"""Get team logo from the configured directory."""
|
||||
try:
|
||||
logo_path = os.path.join(logo_dir, f"{team_abbr}.png")
|
||||
if os.path.exists(logo_path):
|
||||
logo = Image.open(logo_path)
|
||||
return logo
|
||||
else:
|
||||
logger.debug(f"Logo not found: {logo_path}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading logo for {team_abbr}: {e}")
|
||||
return None
|
||||
|
||||
def _fetch_upcoming_games(self) -> List[Dict[str, Any]]:
|
||||
"""Fetch upcoming games with odds for all enabled leagues."""
|
||||
games_data = []
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
for league_key in self.enabled_leagues:
|
||||
if league_key not in self.league_configs:
|
||||
logger.warning(f"Unknown league: {league_key}")
|
||||
continue
|
||||
|
||||
league_config = self.league_configs[league_key]
|
||||
if not league_config['enabled']:
|
||||
logger.debug(f"League {league_key} is disabled, skipping")
|
||||
continue
|
||||
|
||||
try:
|
||||
# Fetch upcoming games for this league
|
||||
games = self._fetch_league_games(league_config, now)
|
||||
games_data.extend(games)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching games for {league_key}: {e}")
|
||||
|
||||
# Sort games by start time
|
||||
games_data.sort(key=lambda x: x.get('start_time', datetime.max))
|
||||
|
||||
# Filter for favorite teams if enabled
|
||||
if self.show_favorite_teams_only:
|
||||
all_favorite_teams = []
|
||||
for league_config in self.league_configs.values():
|
||||
all_favorite_teams.extend(league_config.get('favorite_teams', []))
|
||||
|
||||
games_data = [
|
||||
game for game in games_data
|
||||
if (game.get('home_team') in all_favorite_teams or
|
||||
game.get('away_team') in all_favorite_teams)
|
||||
]
|
||||
|
||||
logger.info(f"Fetched {len(games_data)} upcoming games for odds ticker")
|
||||
return games_data
|
||||
|
||||
def _fetch_league_games(self, league_config: Dict[str, Any], now: datetime) -> List[Dict[str, Any]]:
|
||||
"""Fetch upcoming games for a specific league."""
|
||||
games = []
|
||||
|
||||
# Get dates for API request (today and next 7 days)
|
||||
dates = []
|
||||
for i in range(8): # Today + 7 days
|
||||
date = now + timedelta(days=i)
|
||||
dates.append(date.strftime("%Y%m%d"))
|
||||
|
||||
for date in dates:
|
||||
try:
|
||||
# ESPN API endpoint for games with date parameter
|
||||
sport = league_config['sport']
|
||||
league = league_config['league']
|
||||
url = f"https://site.api.espn.com/apis/site/v2/sports/{sport}/{league}/scoreboard?dates={date}"
|
||||
|
||||
logger.debug(f"Fetching {league} games from ESPN API for date: {date}")
|
||||
response = requests.get(url, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
|
||||
for event in data.get('events', []):
|
||||
game_id = event['id']
|
||||
status = event['status']['type']['name'].lower()
|
||||
|
||||
# Only include upcoming games
|
||||
if status in ['scheduled', 'pre-game']:
|
||||
game_time = datetime.fromisoformat(event['date'].replace('Z', '+00:00'))
|
||||
|
||||
# Only include games in the next 7 days
|
||||
if now <= game_time <= now + timedelta(days=7):
|
||||
# Get team information
|
||||
competitors = event['competitions'][0]['competitors']
|
||||
home_team = next(c for c in competitors if c['homeAway'] == 'home')
|
||||
away_team = next(c for c in competitors if c['homeAway'] == 'away')
|
||||
|
||||
home_abbr = home_team['team']['abbreviation']
|
||||
away_abbr = away_team['team']['abbreviation']
|
||||
|
||||
# Fetch odds for this game
|
||||
odds_data = self.odds_manager.get_odds(
|
||||
sport=sport,
|
||||
league=league,
|
||||
event_id=game_id
|
||||
)
|
||||
|
||||
game_data = {
|
||||
'id': game_id,
|
||||
'league': league_config['league'],
|
||||
'league_key': league_config['sport'],
|
||||
'home_team': home_abbr,
|
||||
'away_team': away_abbr,
|
||||
'start_time': game_time,
|
||||
'odds': odds_data,
|
||||
'logo_dir': league_config['logo_dir']
|
||||
}
|
||||
|
||||
games.append(game_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching {league_config['league']} games for date {date}: {e}")
|
||||
|
||||
return games
|
||||
|
||||
def _format_odds_text(self, game: Dict[str, Any]) -> str:
|
||||
"""Format the odds text for display."""
|
||||
odds = game.get('odds', {})
|
||||
if not odds:
|
||||
return f"{game['away_team']} vs {game['home_team']}"
|
||||
|
||||
# Extract odds data
|
||||
home_team_odds = odds.get('home_team_odds', {})
|
||||
away_team_odds = odds.get('away_team_odds', {})
|
||||
|
||||
home_spread = home_team_odds.get('spread_odds')
|
||||
away_spread = away_team_odds.get('spread_odds')
|
||||
home_ml = home_team_odds.get('money_line')
|
||||
away_ml = away_team_odds.get('money_line')
|
||||
over_under = odds.get('over_under')
|
||||
|
||||
# Format time
|
||||
game_time = game['start_time']
|
||||
timezone_str = self.config.get('timezone', 'UTC')
|
||||
try:
|
||||
tz = pytz.timezone(timezone_str)
|
||||
except pytz.exceptions.UnknownTimeZoneError:
|
||||
tz = pytz.UTC
|
||||
|
||||
if game_time.tzinfo is None:
|
||||
game_time = game_time.replace(tzinfo=pytz.UTC)
|
||||
local_time = game_time.astimezone(tz)
|
||||
time_str = local_time.strftime("%I:%M %p")
|
||||
|
||||
# Build odds string
|
||||
odds_parts = [f"[{time_str}]"]
|
||||
|
||||
# Add away team and odds
|
||||
odds_parts.append(game['away_team'])
|
||||
if away_spread is not None:
|
||||
spread_str = f"{away_spread:+.1f}" if away_spread > 0 else f"{away_spread:.1f}"
|
||||
odds_parts.append(spread_str)
|
||||
if away_ml is not None:
|
||||
ml_str = f"ML {away_ml:+d}" if away_ml > 0 else f"ML {away_ml}"
|
||||
odds_parts.append(ml_str)
|
||||
|
||||
odds_parts.append("vs")
|
||||
|
||||
# Add home team and odds
|
||||
odds_parts.append(game['home_team'])
|
||||
if home_spread is not None:
|
||||
spread_str = f"{home_spread:+.1f}" if home_spread > 0 else f"{home_spread:.1f}"
|
||||
odds_parts.append(spread_str)
|
||||
if home_ml is not None:
|
||||
ml_str = f"ML {home_ml:+d}" if home_ml > 0 else f"ML {home_ml}"
|
||||
odds_parts.append(ml_str)
|
||||
|
||||
# Add over/under
|
||||
if over_under is not None:
|
||||
odds_parts.append(f"O/U {over_under}")
|
||||
|
||||
return " ".join(odds_parts)
|
||||
|
||||
def _create_ticker_image(self, game: Dict[str, Any]) -> Image.Image:
|
||||
"""Create a scrolling ticker image for a game."""
|
||||
width = self.display_manager.matrix.width
|
||||
height = self.display_manager.matrix.height
|
||||
|
||||
# Create a wider image for scrolling
|
||||
scroll_width = width * 3 # 3x width for smooth scrolling
|
||||
image = Image.new('RGB', (scroll_width, height), color=(0, 0, 0))
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
# Format the odds text
|
||||
odds_text = self._format_odds_text(game)
|
||||
|
||||
# Load team logos
|
||||
home_logo = self._get_team_logo(game['home_team'], game['logo_dir'])
|
||||
away_logo = self._get_team_logo(game['away_team'], game['logo_dir'])
|
||||
|
||||
# Calculate text position (start off-screen to the right)
|
||||
text_width = draw.textlength(odds_text, font=self.fonts['medium'])
|
||||
text_x = scroll_width - text_width - 10 # Start off-screen right
|
||||
text_y = (height - self.fonts['medium'].size) // 2
|
||||
|
||||
# Draw the text
|
||||
self._draw_text_with_outline(draw, odds_text, (text_x, text_y), self.fonts['medium'])
|
||||
|
||||
# Add team logos if available
|
||||
logo_size = 16
|
||||
logo_y = (height - logo_size) // 2
|
||||
|
||||
if away_logo:
|
||||
away_logo.thumbnail((logo_size, logo_size), Image.Resampling.LANCZOS)
|
||||
away_x = text_x - logo_size - 5
|
||||
image.paste(away_logo, (away_x, logo_y), away_logo)
|
||||
|
||||
if home_logo:
|
||||
home_logo.thumbnail((logo_size, logo_size), Image.Resampling.LANCZOS)
|
||||
home_x = text_x + text_width + 5
|
||||
image.paste(home_logo, (home_x, logo_y), home_logo)
|
||||
|
||||
return image
|
||||
|
||||
def _draw_text_with_outline(self, draw: ImageDraw.Draw, text: str, position: tuple, font: ImageFont.FreeTypeFont,
|
||||
fill: tuple = (255, 255, 255), outline_color: tuple = (0, 0, 0)) -> None:
|
||||
"""Draw text with a black outline for better readability."""
|
||||
x, y = position
|
||||
# Draw outline
|
||||
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 main text
|
||||
draw.text((x, y), text, font=font, fill=fill)
|
||||
|
||||
def update(self):
|
||||
"""Update odds ticker data."""
|
||||
if not self.is_enabled:
|
||||
return
|
||||
|
||||
current_time = time.time()
|
||||
if current_time - self.last_update < self.update_interval:
|
||||
return
|
||||
|
||||
try:
|
||||
logger.info("Updating odds ticker data")
|
||||
self.games_data = self._fetch_upcoming_games()
|
||||
self.last_update = current_time
|
||||
self.current_position = 0
|
||||
self.current_game_index = 0
|
||||
|
||||
if self.games_data:
|
||||
logger.info(f"Updated odds ticker with {len(self.games_data)} games")
|
||||
else:
|
||||
logger.warning("No games found for odds ticker")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating odds ticker: {e}", exc_info=True)
|
||||
|
||||
def display(self, force_clear: bool = False):
|
||||
"""Display the odds ticker."""
|
||||
if not self.is_enabled or not self.games_data:
|
||||
return
|
||||
|
||||
try:
|
||||
current_time = time.time()
|
||||
|
||||
# Check if it's time to switch games
|
||||
if current_time - self.last_update >= self.display_duration:
|
||||
self.current_game_index = (self.current_game_index + 1) % len(self.games_data)
|
||||
self.current_position = 0
|
||||
self.last_update = current_time
|
||||
force_clear = True
|
||||
|
||||
# Get current game
|
||||
current_game = self.games_data[self.current_game_index]
|
||||
|
||||
# Create ticker image if needed
|
||||
if force_clear or self.current_image is None:
|
||||
self.current_image = self._create_ticker_image(current_game)
|
||||
|
||||
# Scroll the image
|
||||
if current_time - self.last_scroll_time >= self.scroll_delay:
|
||||
self.current_position += self.scroll_speed
|
||||
self.last_scroll_time = current_time
|
||||
|
||||
# Calculate crop region
|
||||
width = self.display_manager.matrix.width
|
||||
height = self.display_manager.matrix.height
|
||||
|
||||
# Reset position when we've scrolled past the end
|
||||
if self.current_position >= self.current_image.width - width:
|
||||
self.current_position = 0
|
||||
|
||||
# Crop the scrolling region
|
||||
crop_x = self.current_position
|
||||
crop_y = 0
|
||||
crop_width = width
|
||||
crop_height = height
|
||||
|
||||
# Ensure we don't go out of bounds
|
||||
if crop_x + crop_width > self.current_image.width:
|
||||
crop_x = self.current_image.width - crop_width
|
||||
|
||||
cropped_image = self.current_image.crop((crop_x, crop_y, crop_x + crop_width, crop_y + crop_height))
|
||||
|
||||
# Display the cropped image
|
||||
self.display_manager.image = cropped_image
|
||||
self.display_manager.draw = ImageDraw.Draw(self.display_manager.image)
|
||||
self.display_manager.update_display()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error displaying odds ticker: {e}", exc_info=True)
|
||||
89
test_odds_ticker.py
Normal file
89
test_odds_ticker.py
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for the OddsTickerManager
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
|
||||
# Add the src directory to the path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
||||
|
||||
from src.display_manager import DisplayManager
|
||||
from src.config_manager import ConfigManager
|
||||
from src.odds_ticker_manager import OddsTickerManager
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s.%(msecs)03d - %(levelname)s:%(name)s:%(message)s',
|
||||
datefmt='%H:%M:%S'
|
||||
)
|
||||
|
||||
def test_odds_ticker():
|
||||
"""Test the odds ticker functionality."""
|
||||
print("Testing OddsTickerManager...")
|
||||
|
||||
try:
|
||||
# Load configuration
|
||||
config_manager = ConfigManager()
|
||||
config = config_manager.load_config()
|
||||
|
||||
# Initialize display manager
|
||||
display_manager = DisplayManager(config)
|
||||
|
||||
# Initialize odds ticker
|
||||
odds_ticker = OddsTickerManager(config, display_manager)
|
||||
|
||||
print(f"Odds ticker enabled: {odds_ticker.is_enabled}")
|
||||
print(f"Enabled leagues: {odds_ticker.enabled_leagues}")
|
||||
print(f"Show favorite teams only: {odds_ticker.show_favorite_teams_only}")
|
||||
|
||||
if not odds_ticker.is_enabled:
|
||||
print("Odds ticker is disabled in config. Enabling for test...")
|
||||
odds_ticker.is_enabled = True
|
||||
|
||||
# Update odds ticker data
|
||||
print("Updating odds ticker data...")
|
||||
odds_ticker.update()
|
||||
|
||||
print(f"Found {len(odds_ticker.games_data)} games")
|
||||
|
||||
if odds_ticker.games_data:
|
||||
print("Sample game data:")
|
||||
for i, game in enumerate(odds_ticker.games_data[:3]): # Show first 3 games
|
||||
print(f" Game {i+1}: {game['away_team']} @ {game['home_team']}")
|
||||
print(f" Time: {game['start_time']}")
|
||||
print(f" League: {game['league']}")
|
||||
if game.get('odds'):
|
||||
print(f" Has odds: Yes")
|
||||
else:
|
||||
print(f" Has odds: No")
|
||||
print()
|
||||
|
||||
# Test display
|
||||
print("Testing display...")
|
||||
for i in range(5): # Display for 5 iterations
|
||||
odds_ticker.display()
|
||||
time.sleep(2)
|
||||
print(f"Display iteration {i+1} complete")
|
||||
|
||||
else:
|
||||
print("No games found. This might be normal if:")
|
||||
print("- No upcoming games in the next 7 days")
|
||||
print("- No favorite teams have upcoming games (if show_favorite_teams_only is True)")
|
||||
print("- API is not returning data")
|
||||
|
||||
# Cleanup
|
||||
display_manager.cleanup()
|
||||
print("Test completed successfully!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during test: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_odds_ticker()
|
||||
Reference in New Issue
Block a user