milb upcoming game debug logging

This commit is contained in:
Chuck
2025-08-10 17:38:56 -05:00
parent ede82406fa
commit 10c1342bdb
10 changed files with 196 additions and 35 deletions

View File

@@ -1033,6 +1033,31 @@ The LEDMatrix system includes a comprehensive scoreboard display system with thr
- Automatic game switching
- Built-in caching to reduce API calls
- Test mode for development
## API Usage Tracking
The LEDMatrix system includes a built-in API usage counter that tracks API calls made by various managers in a 24-hour rolling window. This feature helps monitor API usage and ensure compliance with rate limits.
### API Counter Features
- **Real-time Tracking**: Counts API calls for weather, stocks, sports, and news data
- **24-hour Window**: Rolling window that resets every 24 hours
- **Web Interface Integration**: View current usage in the Overview tab of the web interface
- **Forecast Display**: Shows predicted API usage based on current configuration
- **Automatic Reset**: Counters automatically reset when the 24-hour window expires
### Tracked API Calls
- **Weather**: OpenWeatherMap API calls (geocoding + weather data)
- **Stocks**: Yahoo Finance API calls for stock and crypto data
- **Sports**: ESPN API calls for various sports leagues (NHL, NBA, MLB, NFL, etc.)
- **News**: RSS feed and news API calls
### Accessing API Metrics
1. Open the web interface in your browser
2. Navigate to the **Overview** tab
3. Scroll down to the "API Calls (24h window)" section
4. Click "Refresh API Metrics" to update the display
The counter shows both actual usage and forecasted usage based on your current configuration settings.
## Caching System
The LEDMatrix system includes a robust caching mechanism to optimize API calls and reduce network traffic:

View File

@@ -12,6 +12,13 @@ from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
import pytz
# Import API counter function
try:
from web_interface_v2 import increment_api_counter
except ImportError:
def increment_api_counter(kind: str, count: int = 1):
pass
# Get logger
logger = logging.getLogger(__name__)
@@ -248,6 +255,8 @@ class BaseMiLBManager:
draw = ImageDraw.Draw(image)
# For upcoming games, show date and time stacked in the center
self.logger.debug(f"[MiLB] Game status: {game_data.get('status')}, status_state: {game_data.get('status_state')}")
self.logger.debug(f"[MiLB] Full game data: {game_data}")
if game_data['status'] == 'status_scheduled':
# Ensure game_time_str is defined before use
game_time_str = game_data.get('start_time', '')
@@ -261,48 +270,72 @@ class BaseMiLBManager:
# Draw on the current image
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)
# Update the display
self.display_manager.update_display()
if not game_time_str or 'TBD' in game_time_str:
game_date_str = "TBD"
game_time_formatted_str = ""
self.logger.debug(f"[MiLB] Game time is TBD or empty: {game_time_str}")
else:
game_time = datetime.fromisoformat(game_time_str.replace('Z', '+00:00'))
timezone_str = self.config.get('timezone', 'UTC')
self.logger.debug(f"[MiLB] Processing game time: {game_time_str}")
try:
tz = pytz.timezone(timezone_str)
except pytz.exceptions.UnknownTimeZoneError:
logger.warning(f"Unknown timezone: {timezone_str}, falling back to UTC")
tz = pytz.UTC
if game_time.tzinfo is None:
game_time = game_time.replace(tzinfo=pytz.UTC)
local_time = game_time.astimezone(tz)
# 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_str = local_time.strftime("%-m/%-d")
else:
game_date_str = self.display_manager.format_date_with_ordinal(local_time)
game_time = datetime.fromisoformat(game_time_str.replace('Z', '+00:00'))
timezone_str = self.config.get('timezone', 'UTC')
try:
tz = pytz.timezone(timezone_str)
except pytz.exceptions.UnknownTimeZoneError:
logger.warning(f"Unknown timezone: {timezone_str}, falling back to UTC")
tz = pytz.UTC
if game_time.tzinfo is None:
game_time = game_time.replace(tzinfo=pytz.UTC)
local_time = game_time.astimezone(tz)
self.logger.debug(f"[MiLB] Local time: {local_time}")
# 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_str = local_time.strftime("%-m/%-d")
else:
game_date_str = self.display_manager.format_date_with_ordinal(local_time)
game_time_formatted_str = self._format_game_time(game_data['start_time'])
game_time_formatted_str = self._format_game_time(game_data['start_time'])
self.logger.debug(f"[MiLB] Formatted date: {game_date_str}, time: {game_time_formatted_str}")
except Exception as e:
self.logger.error(f"[MiLB] Error processing game time: {e}")
game_date_str = "TBD"
game_time_formatted_str = "TBD"
# Draw date and time using NHL-style fonts
date_font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8)
time_font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8)
try:
date_font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8)
time_font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8)
self.logger.debug(f"[MiLB] Fonts loaded successfully")
except Exception as e:
self.logger.error(f"[MiLB] Failed to load fonts: {e}")
# Fallback to default font
date_font = ImageFont.load_default()
time_font = ImageFont.load_default()
# Draw date in center
date_width = draw.textlength(game_date_str, font=date_font)
date_x = (width - date_width) // 2
date_y = (height - date_font.size) // 2 - 3
self.logger.debug(f"[MiLB] Drawing date '{game_date_str}' at ({date_x}, {date_y})")
self._draw_text_with_outline(draw, game_date_str, (date_x, date_y), date_font)
# Draw a simple test rectangle to verify drawing is working
draw.rectangle([date_x-2, date_y-2, date_x+date_width+2, date_y+date_font.size+2], outline=(255, 0, 0))
# Draw time below date
time_width = draw.textlength(game_time_formatted_str, font=time_font)
time_x = (width - time_width) // 2
time_y = date_y + 10
self.logger.debug(f"[MiLB] Drawing time '{game_time_formatted_str}' at ({time_x}, {time_y})")
self._draw_text_with_outline(draw, game_time_formatted_str, (time_x, time_y), time_font)
# Draw a simple test rectangle to verify drawing is working
draw.rectangle([time_x-2, time_y-2, time_x+time_width+2, time_y+time_font.size+2], outline=(0, 255, 0))
# For recent/final games, show scores and status
elif game_data['status'] in ['status_final', 'final', 'completed']:
@@ -316,8 +349,6 @@ class BaseMiLBManager:
# Draw on the current image
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)
# Update the display
self.display_manager.update_display()
# Draw scores at the bottom using NHL-style font
away_score = str(game_data['away_score'])
@@ -362,6 +393,7 @@ class BaseMiLBManager:
def _format_game_time(self, game_time: str) -> str:
"""Format game time for display."""
try:
self.logger.debug(f"[MiLB] Formatting game time: {game_time}")
# Get timezone from config
timezone_str = self.config.get('timezone', 'UTC')
try:
@@ -376,7 +408,9 @@ class BaseMiLBManager:
dt = dt.replace(tzinfo=pytz.UTC)
local_dt = dt.astimezone(tz)
return local_dt.strftime("%I:%M%p").lstrip('0')
formatted_time = local_dt.strftime("%I:%M%p").lstrip('0')
self.logger.debug(f"[MiLB] Formatted time: {formatted_time}")
return formatted_time
except Exception as e:
logger.error(f"Error formatting game time: {e}")
return "TBD"
@@ -392,7 +426,9 @@ class BaseMiLBManager:
try:
# Check if test mode is enabled
if self.milb_config.get('test_mode', False):
test_mode = self.milb_config.get('test_mode', False)
self.logger.debug(f"[MiLB] Test mode: {test_mode}")
if test_mode:
self.logger.info("Using test mode data for MiLB")
return {
'test_game_1': {
@@ -417,6 +453,8 @@ class BaseMiLBManager:
current_month = now.month
in_season = 4 <= current_month <= 9
self.logger.debug(f"[MiLB] Current month: {current_month}, in_season: {in_season}")
if not in_season:
self.logger.info("MiLB is currently in offseason (October-March). No games expected.")
self.logger.info("Consider enabling test_mode for offseason testing.")
@@ -440,6 +478,8 @@ class BaseMiLBManager:
response = self.session.get(url, headers=self.headers, timeout=10)
response.raise_for_status()
data = response.json()
# Increment API counter for successful request
increment_api_counter('sports', 1)
except requests.exceptions.RequestException as e:
self.logger.error(f"Error fetching data from {url}: {e}")
continue
@@ -487,6 +527,8 @@ class BaseMiLBManager:
if not event.get('gameDate'):
self.logger.warning(f"Skipping game {game_pk} due to missing 'gameDate'.")
continue
self.logger.debug(f"[MiLB] Game {game_pk} gameDate: {event.get('gameDate')}")
is_favorite_game = (home_abbr in self.favorite_teams or away_abbr in self.favorite_teams)
@@ -522,6 +564,8 @@ class BaseMiLBManager:
'away_record': away_record_str,
'home_record': home_record_str
}
self.logger.debug(f"[MiLB] Created game data for {game_pk}: status={mapped_status}, status_state={mapped_status_state}, start_time={event.get('gameDate')}")
if status_state == 'Live':
linescore = event.get('linescore', {})
@@ -1545,7 +1589,9 @@ class MiLBUpcomingManager(BaseMiLBManager):
return
# --- Optimization: Filter for favorite teams before processing ---
if self.milb_config.get("show_favorite_teams_only", False) and self.favorite_teams:
show_favorite_only = self.milb_config.get("show_favorite_teams_only", False)
self.logger.debug(f"[MiLB] show_favorite_teams_only: {show_favorite_only}, favorite_teams: {self.favorite_teams}")
if show_favorite_only and self.favorite_teams:
games = {
game_id: game for game_id, game in games.items()
if game.get('home_team') in self.favorite_teams or game.get('away_team') in self.favorite_teams
@@ -1572,9 +1618,11 @@ class MiLBUpcomingManager(BaseMiLBManager):
continue
try:
self.logger.debug(f"[MiLB] Parsing start_time: {game['start_time']}")
game_time = datetime.fromisoformat(game['start_time'].replace('Z', '+00:00'))
if game_time.tzinfo is None:
game_time = game_time.replace(tzinfo=timezone.utc)
self.logger.debug(f"[MiLB] Parsed game_time: {game_time}")
except (ValueError, TypeError) as e:
self.logger.error(f"Could not parse start_time for game {game_id}: {game['start_time']}. Error: {e}")
continue
@@ -1593,6 +1641,7 @@ class MiLBUpcomingManager(BaseMiLBManager):
if is_upcoming:
new_upcoming_games.append(game)
self.logger.info(f"[MiLB] Added upcoming game: {game.get('away_team')} @ {game.get('home_team')} at {game_time}")
self.logger.debug(f"[MiLB] Game data for upcoming: {game}")
# Sort by game time (soonest first) and limit to upcoming_games_to_show
new_upcoming_games.sort(key=lambda x: x.get('start_time', ''))
@@ -1622,6 +1671,7 @@ class MiLBUpcomingManager(BaseMiLBManager):
def display(self, force_clear: bool = False):
"""Display upcoming games."""
self.logger.debug(f"[MiLB] Display called with {len(self.upcoming_games)} upcoming games")
if not self.upcoming_games:
current_time = time.time()
if current_time - self.last_warning_time > self.warning_cooldown:
@@ -1639,13 +1689,17 @@ class MiLBUpcomingManager(BaseMiLBManager):
self.current_game = self.upcoming_games[self.current_game_index]
self.last_game_switch = current_time
force_clear = True # Force clear when switching games
self.logger.debug(f"[MiLB] Switched to game {self.current_game_index}: {self.current_game.get('away_team')} @ {self.current_game.get('home_team')}")
# Create and display the game image
if self.current_game:
self.logger.debug(f"[MiLB] Creating display for current game: {self.current_game.get('away_team')} @ {self.current_game.get('home_team')}")
game_image = self._create_game_display(self.current_game)
self.display_manager.image = game_image
self.display_manager.draw = ImageDraw.Draw(self.display_manager.image)
self.display_manager.update_display()
else:
self.logger.debug(f"[MiLB] No current game to display")
except Exception as e:
self.logger.error(f"[MiLB] Error displaying upcoming game: {e}", exc_info=True)

View File

@@ -13,6 +13,14 @@ from urllib3.util.retry import Retry
import pytz
from src.odds_manager import OddsManager
# Import the API counter function from web interface
try:
from web_interface_v2 import increment_api_counter
except ImportError:
# Fallback if web interface is not available
def increment_api_counter(kind: str, count: int = 1):
pass
# Get logger
logger = logging.getLogger(__name__)
@@ -447,6 +455,10 @@ class BaseMLBManager:
response.raise_for_status()
data = response.json()
# Increment API counter for sports data call
increment_api_counter('sports', 1)
self.logger.info(f"Found {len(data.get('events', []))} total games for date {date}")
for event in data.get('events', []):

View File

@@ -13,6 +13,14 @@ from src.config_manager import ConfigManager
from src.odds_manager import OddsManager
import pytz
# Import the API counter function from web interface
try:
from web_interface_v2 import increment_api_counter
except ImportError:
# Fallback if web interface is not available
def increment_api_counter(kind: str, count: int = 1):
pass
# Constants
ESPN_NBA_SCOREBOARD_URL = "https://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard"
@@ -283,6 +291,9 @@ class BaseNBAManager:
response.raise_for_status()
data = response.json()
# Increment API counter for sports data call
increment_api_counter('sports', 1)
if use_cache:
self.cache_manager.set(cache_key, data)

View File

@@ -16,6 +16,14 @@ from src.cache_manager import CacheManager
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# Import the API counter function from web interface
try:
from web_interface_v2 import increment_api_counter
except ImportError:
# Fallback if web interface is not available
def increment_api_counter(kind: str, count: int = 1):
pass
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@@ -107,6 +115,9 @@ class NewsManager:
response = self.session.get(url, headers=headers, timeout=10)
response.raise_for_status()
# Increment API counter for news data call
increment_api_counter('news', 1)
root = ET.fromstring(response.content)
headlines = []

View File

@@ -13,6 +13,14 @@ from src.config_manager import ConfigManager
from src.odds_manager import OddsManager
import pytz
# Import the API counter function from web interface
try:
from web_interface_v2 import increment_api_counter
except ImportError:
# Fallback if web interface is not available
def increment_api_counter(kind: str, count: int = 1):
pass
# Constants
NHL_API_BASE_URL = "https://api-web.nhle.com/v1/schedule/"
@@ -126,6 +134,10 @@ class BaseNHLManager:
response = requests.get(url)
response.raise_for_status()
data = response.json()
# Increment API counter for sports data call
increment_api_counter('sports', 1)
self.logger.info(f"[NHL] Successfully fetched data from NHL API for {date_str}")
# Save to cache if caching is enabled

View File

@@ -15,6 +15,14 @@ from .cache_manager import CacheManager
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# Import the API counter function from web interface
try:
from web_interface_v2 import increment_api_counter
except ImportError:
# Fallback if web interface is not available
def increment_api_counter(kind: str, count: int = 1):
pass
# Get logger without configuring
logger = logging.getLogger(__name__)
@@ -197,6 +205,9 @@ class StockManager:
data = response.json()
# Increment API counter for stock/crypto data call
increment_api_counter('stocks', 1)
# Extract the relevant data from the response
chart_data = data.get('chart', {}).get('result', [{}])[0]
meta = chart_data.get('meta', {})

View File

@@ -14,6 +14,14 @@ from .cache_manager import CacheManager
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# Import the API counter function from web interface
try:
from web_interface_v2 import increment_api_counter
except ImportError:
# Fallback if web interface is not available
def increment_api_counter(kind: str, count: int = 1):
pass
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@@ -125,6 +133,9 @@ class StockNewsManager:
return []
data = response.json()
# Increment API counter for news data call
increment_api_counter('news', 1)
news_items = data.get('news', [])
processed_news = []

View File

@@ -7,6 +7,14 @@ import freetype
from .weather_icons import WeatherIcons
from .cache_manager import CacheManager
# Import the API counter function from web interface
try:
from web_interface_v2 import increment_api_counter
except ImportError:
# Fallback if web interface is not available
def increment_api_counter(kind: str, count: int = 1):
pass
class WeatherManager:
def __init__(self, config: Dict[str, Any], display_manager):
@@ -108,6 +116,9 @@ class WeatherManager:
response.raise_for_status()
geo_data = response.json()
# Increment API counter for geocoding call
increment_api_counter('weather', 1)
if not geo_data:
print(f"Could not find coordinates for {city}, {state}")
return
@@ -123,6 +134,9 @@ class WeatherManager:
response.raise_for_status()
one_call_data = response.json()
# Increment API counter for weather data call
increment_api_counter('weather', 1)
# Store current weather data
self.weather_data = {
'main': {

View File

@@ -881,6 +881,15 @@
</div>
</div>
<h4>API Calls (24h window)</h4>
<div id="api-metrics" class="stat-card" style="text-align:left;">
<div>Loading API metrics...</div>
<div style="margin-top:10px; font-size:12px; color:#666;">If empty, ensure the server is running and /api/metrics is reachable.</div>
<div style="margin-top:10px;">
<button class="btn btn-primary" onclick="updateApiMetrics()"><i class="fas fa-sync"></i> Refresh API Metrics</button>
</div>
</div>
<h4>Quick Actions</h4>
<div class="display-controls">
<button class="btn btn-primary" onclick="systemAction('restart_service')">
@@ -1407,15 +1416,6 @@
Loading features configuration...
</div>
</div>
<h4>API Calls (24h window)</h4>
<div id="api-metrics" class="stat-card" style="text-align:left;">
<div>Loading API metrics...</div>
<div style="margin-top:10px; font-size:12px; color:#666;">If empty, ensure the server is running and /api/metrics is reachable.</div>
<div style="margin-top:10px;">
<button class="btn btn-primary" onclick="updateApiMetrics()"><i class="fas fa-sync"></i> Refresh API Metrics</button>
</div>
</div>
</div>
<!-- Music Tab -->