mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
on demand displays
This commit is contained in:
@@ -318,25 +318,40 @@ class BaseMiLBManager:
|
|||||||
date_font = ImageFont.load_default()
|
date_font = ImageFont.load_default()
|
||||||
time_font = ImageFont.load_default()
|
time_font = ImageFont.load_default()
|
||||||
|
|
||||||
# Draw date in center
|
# Draw date in center (use DisplayManager helpers for compatibility)
|
||||||
date_width = draw.textlength(game_date_str, font=date_font)
|
try:
|
||||||
|
date_width = self.display_manager.get_text_width(game_date_str, date_font)
|
||||||
|
except Exception:
|
||||||
|
# Fallback: approximate width by character count if helper fails
|
||||||
|
date_width = len(game_date_str) * 6
|
||||||
|
date_height = self.display_manager.get_font_height(date_font)
|
||||||
date_x = (width - date_width) // 2
|
date_x = (width - date_width) // 2
|
||||||
date_y = (height - date_font.size) // 2 - 3
|
date_y = (height - date_height) // 2 - 3
|
||||||
self.logger.debug(f"[MiLB] Drawing date '{game_date_str}' at ({date_x}, {date_y})")
|
self.logger.debug(f"[MiLB] Drawing date '{game_date_str}' at ({date_x}, {date_y}), size {date_width}x{date_height}")
|
||||||
self._draw_text_with_outline(draw, game_date_str, (date_x, date_y), date_font)
|
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
|
# Debug rectangle around date text
|
||||||
draw.rectangle([date_x-2, date_y-2, date_x+date_width+2, date_y+date_font.size+2], outline=(255, 0, 0))
|
try:
|
||||||
|
draw.rectangle([date_x-2, date_y-2, date_x+max(0, date_width)+2, date_y+date_height+2], outline=(255, 0, 0))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Draw time below date
|
# Draw time below date
|
||||||
time_width = draw.textlength(game_time_formatted_str, font=time_font)
|
try:
|
||||||
|
time_width = self.display_manager.get_text_width(game_time_formatted_str, time_font)
|
||||||
|
except Exception:
|
||||||
|
time_width = len(game_time_formatted_str) * 6
|
||||||
|
time_height = self.display_manager.get_font_height(time_font)
|
||||||
time_x = (width - time_width) // 2
|
time_x = (width - time_width) // 2
|
||||||
time_y = date_y + 10
|
time_y = date_y + date_height + 2
|
||||||
self.logger.debug(f"[MiLB] Drawing time '{game_time_formatted_str}' at ({time_x}, {time_y})")
|
self.logger.debug(f"[MiLB] Drawing time '{game_time_formatted_str}' at ({time_x}, {time_y}), size {time_width}x{time_height}")
|
||||||
self._draw_text_with_outline(draw, game_time_formatted_str, (time_x, time_y), time_font)
|
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
|
# Debug rectangle around time text
|
||||||
draw.rectangle([time_x-2, time_y-2, time_x+time_width+2, time_y+time_font.size+2], outline=(0, 255, 0))
|
try:
|
||||||
|
draw.rectangle([time_x-2, time_y-2, time_x+max(0, time_width)+2, time_y+time_height+2], outline=(0, 255, 0))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# For recent/final games, show scores and status
|
# For recent/final games, show scores and status
|
||||||
elif game_data['status'] in ['status_final', 'final', 'completed']:
|
elif game_data['status'] in ['status_final', 'final', 'completed']:
|
||||||
|
|||||||
@@ -720,6 +720,8 @@
|
|||||||
<button class="btn btn-primary" onclick="systemAction('restart_service')"><i class="fas fa-redo"></i> Restart Service</button>
|
<button class="btn btn-primary" onclick="systemAction('restart_service')"><i class="fas fa-redo"></i> Restart Service</button>
|
||||||
<button class="btn btn-warning" onclick="systemAction('git_pull')"><i class="fas fa-download"></i> Update Code</button>
|
<button class="btn btn-warning" onclick="systemAction('git_pull')"><i class="fas fa-download"></i> Update Code</button>
|
||||||
<button class="btn btn-danger" onclick="systemAction('reboot_system')"><i class="fas fa-power-off"></i> Reboot</button>
|
<button class="btn btn-danger" onclick="systemAction('reboot_system')"><i class="fas fa-power-off"></i> Reboot</button>
|
||||||
|
<button class="btn btn-secondary" onclick="stopOnDemand()"><i class="fas fa-ban"></i> Stop On-Demand</button>
|
||||||
|
<span id="ondemand-status" style="margin-left:auto; font-size:12px; color:#333; background:#f3f3f3; padding:6px 10px; border-radius:8px;">On-Demand: None</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top:12px; color:#666; font-size:12px;">Service actions may require sudo privileges on the Pi.</div>
|
<div style="margin-top:12px; color:#666; font-size:12px;">Service actions may require sudo privileges on the Pi.</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1147,7 +1149,14 @@
|
|||||||
<!-- Weather Tab -->
|
<!-- Weather Tab -->
|
||||||
<div id="weather" class="tab-content">
|
<div id="weather" class="tab-content">
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
|
<div style="display:flex; justify-content: space-between; align-items:center;">
|
||||||
<h3>Weather Configuration</h3>
|
<h3>Weather Configuration</h3>
|
||||||
|
<div style="display:flex; gap:8px;">
|
||||||
|
<button type="button" class="btn btn-info" onclick="startOnDemand('weather_current')"><i class="fas fa-bolt"></i> On-Demand Current</button>
|
||||||
|
<button type="button" class="btn btn-info" onclick="startOnDemand('weather_hourly')"><i class="fas fa-bolt"></i> Hourly</button>
|
||||||
|
<button type="button" class="btn btn-info" onclick="startOnDemand('weather_daily')"><i class="fas fa-bolt"></i> Daily</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<form id="weather-form">
|
<form id="weather-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>
|
<label>
|
||||||
@@ -1191,7 +1200,12 @@
|
|||||||
<!-- Stocks Tab -->
|
<!-- Stocks Tab -->
|
||||||
<div id="stocks" class="tab-content">
|
<div id="stocks" class="tab-content">
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
|
<div style="display:flex; justify-content: space-between; align-items:center;">
|
||||||
<h3>Stocks & Crypto Configuration</h3>
|
<h3>Stocks & Crypto Configuration</h3>
|
||||||
|
<div style="display:flex; gap:8px;">
|
||||||
|
<button type="button" class="btn btn-info" onclick="startOnDemand('stocks')"><i class="fas fa-bolt"></i> On-Demand Stocks</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<form id="stocks-form">
|
<form id="stocks-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>
|
<label>
|
||||||
@@ -1283,7 +1297,12 @@
|
|||||||
<!-- Stock News Tab -->
|
<!-- Stock News Tab -->
|
||||||
<div id="stocknews" class="tab-content">
|
<div id="stocknews" class="tab-content">
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
|
<div style="display:flex; justify-content: space-between; align-items:center;">
|
||||||
<h3>Stock News</h3>
|
<h3>Stock News</h3>
|
||||||
|
<div style="display:flex; gap:8px;">
|
||||||
|
<button type="button" class="btn btn-info" onclick="startOnDemand('stock_news')"><i class="fas fa-bolt"></i> On-Demand News</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<form id="stocknews-form">
|
<form id="stocknews-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>
|
<label>
|
||||||
@@ -1334,7 +1353,13 @@
|
|||||||
<!-- Odds Ticker Tab -->
|
<!-- Odds Ticker Tab -->
|
||||||
<div id="odds" class="tab-content">
|
<div id="odds" class="tab-content">
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
|
<div style="display:flex; justify-content: space-between; align-items:center;">
|
||||||
<h3>Odds Ticker</h3>
|
<h3>Odds Ticker</h3>
|
||||||
|
<div style="display:flex; gap:8px;">
|
||||||
|
<button type="button" class="btn btn-info" onclick="startOnDemand('odds_ticker')"><i class="fas fa-bolt"></i> On-Demand</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="stopOnDemand()"><i class="fas fa-ban"></i> Stop</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<form id="odds-form">
|
<form id="odds-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>
|
<label>
|
||||||
@@ -1386,7 +1411,12 @@
|
|||||||
<!-- Text Display Tab -->
|
<!-- Text Display Tab -->
|
||||||
<div id="text" class="tab-content">
|
<div id="text" class="tab-content">
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
|
<div style="display:flex; justify-content: space-between; align-items:center;">
|
||||||
<h3>Text Display</h3>
|
<h3>Text Display</h3>
|
||||||
|
<div style="display:flex; gap:8px;">
|
||||||
|
<button type="button" class="btn btn-info" onclick="startOnDemand('text_display')"><i class="fas fa-bolt"></i> On-Demand</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<form id="text-form">
|
<form id="text-form">
|
||||||
<div class="form-group"><label><input type="checkbox" id="text_enabled" {% if main_config.text_display.enabled %}checked{% endif %}> Enable</label></div>
|
<div class="form-group"><label><input type="checkbox" id="text_enabled" {% if main_config.text_display.enabled %}checked{% endif %}> Enable</label></div>
|
||||||
<div class="form-group"><label for="text_text">Text</label><input type="text" id="text_text" class="form-control" value="{{ main_config.text_display.text }}"></div>
|
<div class="form-group"><label for="text_text">Text</label><input type="text" id="text_text" class="form-control" value="{{ main_config.text_display.text }}"></div>
|
||||||
@@ -1455,7 +1485,12 @@
|
|||||||
<!-- YouTube Tab -->
|
<!-- YouTube Tab -->
|
||||||
<div id="youtube" class="tab-content">
|
<div id="youtube" class="tab-content">
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
|
<div style="display:flex; justify-content: space-between; align-items:center;">
|
||||||
<h3>YouTube</h3>
|
<h3>YouTube</h3>
|
||||||
|
<div style="display:flex; gap:8px;">
|
||||||
|
<button type="button" class="btn btn-info" onclick="startOnDemand('youtube')"><i class="fas fa-bolt"></i> On-Demand</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<form id="youtube-form">
|
<form id="youtube-form">
|
||||||
<div class="form-group"><label><input type="checkbox" id="youtube_enabled" {% if main_config.youtube.enabled %}checked{% endif %}> Enable YouTube</label></div>
|
<div class="form-group"><label><input type="checkbox" id="youtube_enabled" {% if main_config.youtube.enabled %}checked{% endif %}> Enable YouTube</label></div>
|
||||||
<div class="form-group"><label for="youtube_update_interval">Update Interval (sec)</label><input type="number" id="youtube_update_interval" class="form-control" value="{{ main_config.youtube.update_interval }}"></div>
|
<div class="form-group"><label for="youtube_update_interval">Update Interval (sec)</label><input type="number" id="youtube_update_interval" class="form-control" value="{{ main_config.youtube.update_interval }}"></div>
|
||||||
@@ -1467,7 +1502,12 @@
|
|||||||
<!-- Calendar Tab -->
|
<!-- Calendar Tab -->
|
||||||
<div id="calendar" class="tab-content">
|
<div id="calendar" class="tab-content">
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
|
<div style="display:flex; justify-content: space-between; align-items:center;">
|
||||||
<h3>Calendar Configuration</h3>
|
<h3>Calendar Configuration</h3>
|
||||||
|
<div style="display:flex; gap:8px;">
|
||||||
|
<button type="button" class="btn btn-info" onclick="startOnDemand('calendar')"><i class="fas fa-bolt"></i> On-Demand</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<form id="calendar-form">
|
<form id="calendar-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>
|
<label>
|
||||||
@@ -1498,7 +1538,12 @@
|
|||||||
<!-- News Tab -->
|
<!-- News Tab -->
|
||||||
<div id="news" class="tab-content">
|
<div id="news" class="tab-content">
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
|
<div style="display:flex; justify-content: space-between; align-items:center;">
|
||||||
<h3>News Manager Configuration</h3>
|
<h3>News Manager Configuration</h3>
|
||||||
|
<div style="display:flex; gap:8px;">
|
||||||
|
<button type="button" class="btn btn-info" onclick="startOnDemand('news_manager')"><i class="fas fa-bolt"></i> On-Demand</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<p>Configure RSS news feeds and scrolling ticker settings</p>
|
<p>Configure RSS news feeds and scrolling ticker settings</p>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -1775,6 +1820,44 @@
|
|||||||
let currentElements = [];
|
let currentElements = [];
|
||||||
let selectedElement = null;
|
let selectedElement = null;
|
||||||
|
|
||||||
|
async function refreshOnDemandStatus(){
|
||||||
|
try{
|
||||||
|
const res = await fetch('/api/ondemand/status');
|
||||||
|
const data = await res.json();
|
||||||
|
if(data && data.on_demand){
|
||||||
|
const s = data.on_demand;
|
||||||
|
const el = document.getElementById('ondemand-status');
|
||||||
|
if(el){ el.textContent = `On-Demand: ${s.running && s.mode ? s.mode : 'None'}`; }
|
||||||
|
}
|
||||||
|
}catch(e){ /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startOnDemand(mode){
|
||||||
|
try{
|
||||||
|
const res = await fetch('/api/ondemand/start', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ mode })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
showNotification(data.message || 'Requested on-demand start', data.status || 'success');
|
||||||
|
refreshOnDemandStatus();
|
||||||
|
}catch(err){
|
||||||
|
showNotification('Error starting on-demand: ' + err, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopOnDemand(){
|
||||||
|
try{
|
||||||
|
const res = await fetch('/api/ondemand/stop', { method: 'POST' });
|
||||||
|
const data = await res.json();
|
||||||
|
showNotification(data.message || 'On-Demand stopped', data.status || 'success');
|
||||||
|
refreshOnDemandStatus();
|
||||||
|
}catch(err){
|
||||||
|
showNotification('Error stopping on-demand: ' + err, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize on page load
|
// Initialize on page load
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
initializeSocket();
|
initializeSocket();
|
||||||
@@ -1782,6 +1865,7 @@
|
|||||||
updateSystemStats();
|
updateSystemStats();
|
||||||
loadNewsManagerData();
|
loadNewsManagerData();
|
||||||
updateApiMetrics();
|
updateApiMetrics();
|
||||||
|
refreshOnDemandStatus();
|
||||||
|
|
||||||
// UI controls for grid & scale
|
// UI controls for grid & scale
|
||||||
const scaleRange = document.getElementById('scaleRange');
|
const scaleRange = document.getElementById('scaleRange');
|
||||||
@@ -2119,6 +2203,7 @@
|
|||||||
validateJson('secrets-config-json', 'secrets-config-validation');
|
validateJson('secrets-config-json', 'secrets-config-validation');
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
refreshOnDemandStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display control functions
|
// Display control functions
|
||||||
@@ -2401,6 +2486,7 @@
|
|||||||
document.querySelectorAll('.stat-card .stat-value')[2].textContent = stats.cpu_temp + '°C';
|
document.querySelectorAll('.stat-card .stat-value')[2].textContent = stats.cpu_temp + '°C';
|
||||||
document.querySelectorAll('.stat-card .stat-value')[5].textContent = stats.disk_used_percent + '%';
|
document.querySelectorAll('.stat-card .stat-value')[5].textContent = stats.disk_used_percent + '%';
|
||||||
}
|
}
|
||||||
|
refreshOnDemandStatus();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating system stats:', error);
|
console.error('Error updating system stats:', error);
|
||||||
}
|
}
|
||||||
@@ -3017,6 +3103,17 @@
|
|||||||
const stats = await res.json();
|
const stats = await res.json();
|
||||||
// Build a minimal sports UI off current config
|
// Build a minimal sports UI off current config
|
||||||
const cfg = currentConfig;
|
const cfg = currentConfig;
|
||||||
|
const leaguePrefixes = {
|
||||||
|
'nfl_scoreboard': 'nfl',
|
||||||
|
'mlb': 'mlb',
|
||||||
|
'milb': 'milb',
|
||||||
|
'nhl_scoreboard': 'nhl',
|
||||||
|
'nba_scoreboard': 'nba',
|
||||||
|
'ncaa_fb_scoreboard': 'ncaa_fb',
|
||||||
|
'ncaa_baseball_scoreboard': 'ncaa_baseball',
|
||||||
|
'ncaam_basketball_scoreboard': 'ncaam_basketball',
|
||||||
|
'soccer_scoreboard': 'soccer'
|
||||||
|
};
|
||||||
const leagues = [
|
const leagues = [
|
||||||
{ key: 'nfl_scoreboard', label: 'NFL' },
|
{ key: 'nfl_scoreboard', label: 'NFL' },
|
||||||
{ key: 'mlb', label: 'MLB' },
|
{ key: 'mlb', label: 'MLB' },
|
||||||
@@ -3031,6 +3128,7 @@
|
|||||||
const container = document.getElementById('sports-config');
|
const container = document.getElementById('sports-config');
|
||||||
const html = leagues.map(l => {
|
const html = leagues.map(l => {
|
||||||
const sec = cfg[l.key] || {};
|
const sec = cfg[l.key] || {};
|
||||||
|
const p = leaguePrefixes[l.key] || l.key;
|
||||||
const fav = (sec.favorite_teams || []).join(', ');
|
const fav = (sec.favorite_teams || []).join(', ');
|
||||||
const recentToShow = sec.recent_games_to_show ?? 1;
|
const recentToShow = sec.recent_games_to_show ?? 1;
|
||||||
const upcomingToShow = sec.upcoming_games_to_show ?? 1;
|
const upcomingToShow = sec.upcoming_games_to_show ?? 1;
|
||||||
@@ -3039,10 +3137,18 @@
|
|||||||
const upcomingUpd = sec.upcoming_update_interval ?? 3600;
|
const upcomingUpd = sec.upcoming_update_interval ?? 3600;
|
||||||
return `
|
return `
|
||||||
<div style="border:1px solid #ddd; border-radius:6px; padding:12px; margin:10px 0;">
|
<div style="border:1px solid #ddd; border-radius:6px; padding:12px; margin:10px 0;">
|
||||||
<label style="display:flex; align-items:center; gap:8px;">
|
<div style="display:flex; justify-content: space-between; align-items:center; margin-bottom:8px;">
|
||||||
|
<label style="display:flex; align-items:center; gap:8px; margin:0;">
|
||||||
<input type="checkbox" data-league="${l.key}" class="sp-enabled" ${sec.enabled ? 'checked' : ''}>
|
<input type="checkbox" data-league="${l.key}" class="sp-enabled" ${sec.enabled ? 'checked' : ''}>
|
||||||
<strong>${l.label}</strong>
|
<strong>${l.label}</strong>
|
||||||
</label>
|
</label>
|
||||||
|
<div style="display:flex; gap:6px;">
|
||||||
|
<button type="button" class="btn btn-info" onclick="startOnDemand('${p}_live')"><i class="fas fa-bolt"></i> Live</button>
|
||||||
|
<button type="button" class="btn btn-info" onclick="startOnDemand('${p}_recent')"><i class="fas fa-bolt"></i> Recent</button>
|
||||||
|
<button type="button" class="btn btn-info" onclick="startOnDemand('${p}_upcoming')"><i class="fas fa-bolt"></i> Upcoming</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="stopOnDemand()"><i class="fas fa-ban"></i> Stop</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-row" style="margin-top:10px;">
|
<div class="form-row" style="margin-top:10px;">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Live Priority</label>
|
<label>Live Priority</label>
|
||||||
|
|||||||
@@ -11,6 +11,25 @@ import psutil
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from src.config_manager import ConfigManager
|
from src.config_manager import ConfigManager
|
||||||
from src.display_manager import DisplayManager
|
from src.display_manager import DisplayManager
|
||||||
|
from src.cache_manager import CacheManager
|
||||||
|
from src.clock import Clock
|
||||||
|
from src.weather_manager import WeatherManager
|
||||||
|
from src.stock_manager import StockManager
|
||||||
|
from src.stock_news_manager import StockNewsManager
|
||||||
|
from src.odds_ticker_manager import OddsTickerManager
|
||||||
|
from src.calendar_manager import CalendarManager
|
||||||
|
from src.youtube_display import YouTubeDisplay
|
||||||
|
from src.text_display import TextDisplay
|
||||||
|
from src.news_manager import NewsManager
|
||||||
|
from src.nhl_managers import NHLLiveManager, NHLRecentManager, NHLUpcomingManager
|
||||||
|
from src.nba_managers import NBALiveManager, NBARecentManager, NBAUpcomingManager
|
||||||
|
from src.mlb_manager import MLBLiveManager, MLBRecentManager, MLBUpcomingManager
|
||||||
|
from src.milb_manager import MiLBLiveManager, MiLBRecentManager, MiLBUpcomingManager
|
||||||
|
from src.soccer_managers import SoccerLiveManager, SoccerRecentManager, SoccerUpcomingManager
|
||||||
|
from src.nfl_managers import NFLLiveManager, NFLRecentManager, NFLUpcomingManager
|
||||||
|
from src.ncaa_fb_managers import NCAAFBLiveManager, NCAAFBRecentManager, NCAAFBUpcomingManager
|
||||||
|
from src.ncaa_baseball_managers import NCAABaseballLiveManager, NCAABaseballRecentManager, NCAABaseballUpcomingManager
|
||||||
|
from src.ncaam_basketball_managers import NCAAMBasketballLiveManager, NCAAMBasketballRecentManager, NCAAMBasketballUpcomingManager
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import io
|
import io
|
||||||
import signal
|
import signal
|
||||||
@@ -111,6 +130,214 @@ class DisplayMonitor:
|
|||||||
|
|
||||||
display_monitor = DisplayMonitor()
|
display_monitor = DisplayMonitor()
|
||||||
|
|
||||||
|
|
||||||
|
class OnDemandRunner:
|
||||||
|
"""Run a single display mode on demand until stopped."""
|
||||||
|
def __init__(self):
|
||||||
|
self.running = False
|
||||||
|
self.thread = None
|
||||||
|
self.mode = None
|
||||||
|
self.force_clear_next = False
|
||||||
|
self.cache_manager = None
|
||||||
|
self.config = None
|
||||||
|
|
||||||
|
def _ensure_infra(self):
|
||||||
|
"""Ensure config, cache, and display manager are initialized."""
|
||||||
|
global display_manager
|
||||||
|
if self.cache_manager is None:
|
||||||
|
self.cache_manager = CacheManager()
|
||||||
|
if self.config is None:
|
||||||
|
self.config = config_manager.load_config()
|
||||||
|
if not display_manager:
|
||||||
|
# Initialize with hardware if possible
|
||||||
|
try:
|
||||||
|
display_manager = DisplayManager(self.config)
|
||||||
|
except Exception:
|
||||||
|
display_manager = DisplayManager({'display': {'hardware': {}}}, force_fallback=True)
|
||||||
|
display_monitor.start()
|
||||||
|
|
||||||
|
def _is_service_active(self) -> bool:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['systemctl', 'is-active', 'ledmatrix'], capture_output=True, text=True)
|
||||||
|
return result.stdout.strip() == 'active'
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def start(self, mode: str):
|
||||||
|
"""Start on-demand mode. Throws RuntimeError if service is active."""
|
||||||
|
if self._is_service_active():
|
||||||
|
raise RuntimeError('LEDMatrix service is active. Stop it first to use On-Demand.')
|
||||||
|
|
||||||
|
# If already running same mode, no-op
|
||||||
|
if self.running and self.mode == mode:
|
||||||
|
return
|
||||||
|
# Switch from previous
|
||||||
|
if self.running:
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
self._ensure_infra()
|
||||||
|
self.mode = mode
|
||||||
|
self.running = True
|
||||||
|
self.force_clear_next = True
|
||||||
|
# Use SocketIO bg task for cooperative sleeping
|
||||||
|
self.thread = socketio.start_background_task(self._run_loop)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.running = False
|
||||||
|
self.mode = None
|
||||||
|
self.thread = None
|
||||||
|
|
||||||
|
def status(self) -> dict:
|
||||||
|
return {
|
||||||
|
'running': self.running,
|
||||||
|
'mode': self.mode,
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Mode construction helpers ---
|
||||||
|
def _build_manager(self, mode: str):
|
||||||
|
global display_manager
|
||||||
|
cfg = self.config or {}
|
||||||
|
# Non-sport managers
|
||||||
|
if mode == 'clock':
|
||||||
|
mgr = Clock(display_manager)
|
||||||
|
self._force_enable(mgr)
|
||||||
|
return mgr, lambda fc=False: mgr.display_time(force_clear=fc), None, 1.0
|
||||||
|
if mode == 'weather_current':
|
||||||
|
mgr = WeatherManager(cfg, display_manager)
|
||||||
|
self._force_enable(mgr)
|
||||||
|
return mgr, lambda fc=False: mgr.display_weather(force_clear=fc), lambda: mgr.get_weather(), float(cfg.get('weather', {}).get('update_interval', 1800))
|
||||||
|
if mode == 'weather_hourly':
|
||||||
|
mgr = WeatherManager(cfg, display_manager)
|
||||||
|
self._force_enable(mgr)
|
||||||
|
return mgr, lambda fc=False: mgr.display_hourly_forecast(force_clear=fc), lambda: mgr.get_weather(), float(cfg.get('weather', {}).get('update_interval', 1800))
|
||||||
|
if mode == 'weather_daily':
|
||||||
|
mgr = WeatherManager(cfg, display_manager)
|
||||||
|
self._force_enable(mgr)
|
||||||
|
return mgr, lambda fc=False: mgr.display_daily_forecast(force_clear=fc), lambda: mgr.get_weather(), float(cfg.get('weather', {}).get('update_interval', 1800))
|
||||||
|
if mode == 'stocks':
|
||||||
|
mgr = StockManager(cfg, display_manager)
|
||||||
|
self._force_enable(mgr)
|
||||||
|
return mgr, lambda fc=False: mgr.display_stocks(force_clear=fc), lambda: mgr.update_stock_data(), float(cfg.get('stocks', {}).get('update_interval', 600))
|
||||||
|
if mode == 'stock_news':
|
||||||
|
mgr = StockNewsManager(cfg, display_manager)
|
||||||
|
self._force_enable(mgr)
|
||||||
|
return mgr, lambda fc=False: mgr.display_news(), lambda: mgr.update_news_data(), float(cfg.get('stock_news', {}).get('update_interval', 300))
|
||||||
|
if mode == 'odds_ticker':
|
||||||
|
mgr = OddsTickerManager(cfg, display_manager)
|
||||||
|
self._force_enable(mgr)
|
||||||
|
return mgr, lambda fc=False: mgr.display(force_clear=fc), lambda: mgr.update(), float(cfg.get('odds_ticker', {}).get('update_interval', 300))
|
||||||
|
if mode == 'calendar':
|
||||||
|
mgr = CalendarManager(display_manager, cfg)
|
||||||
|
self._force_enable(mgr)
|
||||||
|
return mgr, lambda fc=False: mgr.display(force_clear=fc), lambda: mgr.update(time.time()), 60.0
|
||||||
|
if mode == 'youtube':
|
||||||
|
mgr = YouTubeDisplay(display_manager, cfg)
|
||||||
|
self._force_enable(mgr)
|
||||||
|
return mgr, lambda fc=False: mgr.display(force_clear=fc), lambda: mgr.update(), float(cfg.get('youtube', {}).get('update_interval', 30))
|
||||||
|
if mode == 'text_display':
|
||||||
|
mgr = TextDisplay(display_manager, cfg)
|
||||||
|
self._force_enable(mgr)
|
||||||
|
return mgr, lambda fc=False: mgr.display(), lambda: getattr(mgr, 'update', lambda: None)(), 5.0
|
||||||
|
if mode == 'of_the_day':
|
||||||
|
from src.of_the_day_manager import OfTheDayManager # local import to avoid circulars
|
||||||
|
mgr = OfTheDayManager(display_manager, cfg)
|
||||||
|
self._force_enable(mgr)
|
||||||
|
return mgr, lambda fc=False: mgr.display(force_clear=fc), lambda: mgr.update(time.time()), 300.0
|
||||||
|
if mode == 'news_manager':
|
||||||
|
mgr = NewsManager(cfg, display_manager)
|
||||||
|
self._force_enable(mgr)
|
||||||
|
return mgr, lambda fc=False: mgr.display_news(), None, 0
|
||||||
|
|
||||||
|
# Sports managers mapping helper
|
||||||
|
def sport(kind: str, variant: str):
|
||||||
|
# kind examples: nhl, nba, mlb, milb, soccer, nfl, ncaa_fb, ncaa_baseball, ncaam_basketball
|
||||||
|
# variant: live/recent/upcoming
|
||||||
|
if kind == 'nhl':
|
||||||
|
cls = {'live': NHLLiveManager, 'recent': NHLRecentManager, 'upcoming': NHLUpcomingManager}[variant]
|
||||||
|
elif kind == 'nba':
|
||||||
|
cls = {'live': NBALiveManager, 'recent': NBARecentManager, 'upcoming': NBAUpcomingManager}[variant]
|
||||||
|
elif kind == 'mlb':
|
||||||
|
cls = {'live': MLBLiveManager, 'recent': MLBRecentManager, 'upcoming': MLBUpcomingManager}[variant]
|
||||||
|
elif kind == 'milb':
|
||||||
|
cls = {'live': MiLBLiveManager, 'recent': MiLBRecentManager, 'upcoming': MiLBUpcomingManager}[variant]
|
||||||
|
elif kind == 'soccer':
|
||||||
|
cls = {'live': SoccerLiveManager, 'recent': SoccerRecentManager, 'upcoming': SoccerUpcomingManager}[variant]
|
||||||
|
elif kind == 'nfl':
|
||||||
|
cls = {'live': NFLLiveManager, 'recent': NFLRecentManager, 'upcoming': NFLUpcomingManager}[variant]
|
||||||
|
elif kind == 'ncaa_fb':
|
||||||
|
cls = {'live': NCAAFBLiveManager, 'recent': NCAAFBRecentManager, 'upcoming': NCAAFBUpcomingManager}[variant]
|
||||||
|
elif kind == 'ncaa_baseball':
|
||||||
|
cls = {'live': NCAABaseballLiveManager, 'recent': NCAABaseballRecentManager, 'upcoming': NCAABaseballUpcomingManager}[variant]
|
||||||
|
elif kind == 'ncaam_basketball':
|
||||||
|
cls = {'live': NCAAMBasketballLiveManager, 'recent': NCAAMBasketballRecentManager, 'upcoming': NCAAMBasketballUpcomingManager}[variant]
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported sport kind: {kind}")
|
||||||
|
mgr = cls(cfg, display_manager, self.cache_manager)
|
||||||
|
self._force_enable(mgr)
|
||||||
|
return mgr, lambda fc=False: mgr.display(force_clear=fc), lambda: mgr.update(), float(getattr(mgr, 'update_interval', 60))
|
||||||
|
|
||||||
|
if mode.endswith('_live'):
|
||||||
|
return sport(mode.replace('_live', ''), 'live')
|
||||||
|
if mode.endswith('_recent'):
|
||||||
|
return sport(mode.replace('_recent', ''), 'recent')
|
||||||
|
if mode.endswith('_upcoming'):
|
||||||
|
return sport(mode.replace('_upcoming', ''), 'upcoming')
|
||||||
|
|
||||||
|
raise ValueError(f"Unknown on-demand mode: {mode}")
|
||||||
|
|
||||||
|
def _force_enable(self, mgr):
|
||||||
|
try:
|
||||||
|
if hasattr(mgr, 'is_enabled'):
|
||||||
|
setattr(mgr, 'is_enabled', True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _run_loop(self):
|
||||||
|
"""Background loop: update and display selected mode until stopped."""
|
||||||
|
mode = self.mode
|
||||||
|
try:
|
||||||
|
manager, display_fn, update_fn, update_interval = self._build_manager(mode)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to initialize on-demand manager for mode {mode}: {e}")
|
||||||
|
self.running = False
|
||||||
|
return
|
||||||
|
|
||||||
|
last_update = 0.0
|
||||||
|
while self.running and self.mode == mode:
|
||||||
|
try:
|
||||||
|
now = time.time()
|
||||||
|
if update_fn and (now - last_update >= max(1e-3, update_interval)):
|
||||||
|
update_fn()
|
||||||
|
last_update = now
|
||||||
|
|
||||||
|
# Call display frequently for smooth animation where applicable
|
||||||
|
try:
|
||||||
|
display_fn(self.force_clear_next)
|
||||||
|
except TypeError:
|
||||||
|
# Fallback if callable ignores force_clear
|
||||||
|
display_fn()
|
||||||
|
|
||||||
|
if self.force_clear_next:
|
||||||
|
self.force_clear_next = False
|
||||||
|
except Exception as loop_err:
|
||||||
|
logger.error(f"Error in on-demand loop for {mode}: {loop_err}")
|
||||||
|
# small backoff to avoid tight error loop
|
||||||
|
try:
|
||||||
|
socketio.sleep(0.5)
|
||||||
|
except Exception:
|
||||||
|
time.sleep(0.5)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Target higher FPS for ticker; moderate for others
|
||||||
|
sleep_seconds = 0.02 if mode == 'odds_ticker' else 0.08
|
||||||
|
try:
|
||||||
|
socketio.sleep(sleep_seconds)
|
||||||
|
except Exception:
|
||||||
|
time.sleep(sleep_seconds)
|
||||||
|
|
||||||
|
|
||||||
|
on_demand_runner = OnDemandRunner()
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
try:
|
try:
|
||||||
@@ -185,7 +412,7 @@ def get_system_status():
|
|||||||
disk = psutil.disk_usage('/')
|
disk = psutil.disk_usage('/')
|
||||||
disk_used_percent = round((disk.used / disk.total) * 100, 1)
|
disk_used_percent = round((disk.used / disk.total) * 100, 1)
|
||||||
|
|
||||||
return {
|
status = {
|
||||||
'service_active': service_active,
|
'service_active': service_active,
|
||||||
'memory_used_percent': mem_used_percent,
|
'memory_used_percent': mem_used_percent,
|
||||||
'cpu_percent': cpu_percent,
|
'cpu_percent': cpu_percent,
|
||||||
@@ -193,8 +420,10 @@ def get_system_status():
|
|||||||
'disk_used_percent': disk_used_percent,
|
'disk_used_percent': disk_used_percent,
|
||||||
'uptime': f"{uptime_hours}h {uptime_minutes}m",
|
'uptime': f"{uptime_hours}h {uptime_minutes}m",
|
||||||
'display_connected': display_manager is not None,
|
'display_connected': display_manager is not None,
|
||||||
'editor_mode': editor_mode
|
'editor_mode': editor_mode,
|
||||||
|
'on_demand': on_demand_runner.status()
|
||||||
}
|
}
|
||||||
|
return status
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {
|
return {
|
||||||
'service_active': False,
|
'service_active': False,
|
||||||
@@ -481,6 +710,39 @@ def get_system_status_api():
|
|||||||
"""Get system status as JSON."""
|
"""Get system status as JSON."""
|
||||||
return jsonify(get_system_status())
|
return jsonify(get_system_status())
|
||||||
|
|
||||||
|
# --- On-Demand Controls ---
|
||||||
|
@app.route('/api/ondemand/start', methods=['POST'])
|
||||||
|
def api_ondemand_start():
|
||||||
|
try:
|
||||||
|
data = request.get_json(force=True)
|
||||||
|
mode = (data or {}).get('mode')
|
||||||
|
if not mode:
|
||||||
|
return jsonify({'status': 'error', 'message': 'Missing mode'}), 400
|
||||||
|
# Refuse if service is running
|
||||||
|
if on_demand_runner._is_service_active():
|
||||||
|
return jsonify({'status': 'error', 'message': 'Service is active. Stop it first to use On-Demand.'}), 400
|
||||||
|
on_demand_runner.start(mode)
|
||||||
|
return jsonify({'status': 'success', 'message': f'On-Demand started: {mode}', 'on_demand': on_demand_runner.status()})
|
||||||
|
except RuntimeError as rte:
|
||||||
|
return jsonify({'status': 'error', 'message': str(rte)}), 400
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'status': 'error', 'message': f'Error starting on-demand: {e}'}), 500
|
||||||
|
|
||||||
|
@app.route('/api/ondemand/stop', methods=['POST'])
|
||||||
|
def api_ondemand_stop():
|
||||||
|
try:
|
||||||
|
on_demand_runner.stop()
|
||||||
|
return jsonify({'status': 'success', 'message': 'On-Demand stopped', 'on_demand': on_demand_runner.status()})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'status': 'error', 'message': f'Error stopping on-demand: {e}'}), 500
|
||||||
|
|
||||||
|
@app.route('/api/ondemand/status', methods=['GET'])
|
||||||
|
def api_ondemand_status():
|
||||||
|
try:
|
||||||
|
return jsonify({'status': 'success', 'on_demand': on_demand_runner.status()})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||||
|
|
||||||
# --- API Call Metrics (simple in-memory counters) ---
|
# --- API Call Metrics (simple in-memory counters) ---
|
||||||
api_counters = {
|
api_counters = {
|
||||||
'weather': {'used': 0},
|
'weather': {'used': 0},
|
||||||
|
|||||||
Reference in New Issue
Block a user