feat: Add automated game timer with --start-timer command line switch

- Add --start-timer command line argument with configurable delay (default 4 minutes)
- Implement MatchTimerComponent integration with proper initialization and registration
- Add timer state API endpoints for web dashboard integration
- Enhance message bus communication between timer component and web dashboard
- Update documentation and changelog with comprehensive timer feature details
- Fix timer component not being initialized during application startup
- Add timer state caching and real-time API endpoint integration

The timer automatically sends START_GAME messages after the specified delay,
enabling automated match progression without manual intervention.
parent 17a5dc02
......@@ -2,6 +2,33 @@
All notable changes to this project will be documented in this file.
## [1.2.6] - 2025-08-26
### Added
- **Automated Game Timer**: New `--start-timer` command line switch that sends START_GAME message after specified delay (default 4 minutes)
- **MatchTimerComponent Integration**: Fixed timer component initialization and registration with thread manager
- **Timer State API**: Real-time timer state API endpoints for web dashboard integration
- **Message Bus Communication**: Proper inter-thread communication between timer component and web dashboard
- **Timer Configuration**: Configurable timer delay with command-line parameter support
### Enhanced
- **Application Architecture**: Enhanced timer system with proper component lifecycle management
- **Web Dashboard Integration**: Timer state caching and API endpoint integration
- **Command Line Interface**: Extended CLI with timer configuration options
- **Documentation**: Comprehensive timer feature documentation and usage examples
### Fixed
- **MatchTimerComponent Registration**: Fixed timer component not being initialized during application startup
- **Timer State Communication**: Resolved web dashboard timer state requests not reaching timer component
- **API Endpoint Integration**: Fixed timer state API endpoints returning placeholder data
### Technical Details
- **Timer Architecture**: Implemented threading.Timer with proper daemon thread management
- **Message Bus Integration**: Added CUSTOM message type handling for timer state requests
- **Settings Integration**: Added TimerConfig dataclass with database persistence support
- **Web Dashboard Enhancement**: Added timer state caching and message handler for timer responses
- **Cross-Platform Compatibility**: Timer system works consistently across Windows, Linux, and macOS
## [1.2.5] - 2025-08-26
### Added
......
......@@ -196,6 +196,12 @@ FLASK_DEBUG=False
# Basic usage (screen casting enabled by default)
python main.py
# Start with automated game timer (4 minutes default)
python main.py --start-timer
# Start with custom timer delay (2 minutes)
python main.py --start-timer 2
# Available options
python main.py [OPTIONS]
......@@ -209,6 +215,7 @@ Options:
--no-web Disable web dashboard
--no-api Disable API client
--no-screen-cast Disable screen casting functionality
--start-timer INTEGER Start timer in minutes to send START_GAME message [default: 4]
--database-path TEXT Custom database path
--log-level TEXT Logging level [default: INFO]
--config-dir TEXT Custom configuration directory
......@@ -684,7 +691,7 @@ Content-Type: application/json
## Match Timer System
The application includes a comprehensive match timer system with automatic match progression and visual countdown displays.
The application includes a comprehensive match timer system with automatic match progression, visual countdown displays, and command-line timer configuration.
### Match Timer Features
......@@ -770,6 +777,59 @@ Timer appearance is controlled via CSS variables and classes:
}
```
### Command-Line Timer Configuration
The application supports automated game start via command-line timer configuration:
#### --start-timer Option
```bash
# Start with default 4-minute timer
python main.py --start-timer
# Start with custom timer delay (in minutes)
python main.py --start-timer 2
# Start with 30-second timer for testing
python main.py --start-timer 0.5
```
#### Timer Behavior
- **Automatic START_GAME**: When the timer expires, a START_GAME message is automatically sent to the message bus
- **MatchTimerComponent Integration**: The command-line timer works seamlessly with the existing MatchTimerComponent
- **Web Interface Display**: Timer state is available through the web dashboard API endpoints
- **Configurable Delay**: Timer delay can be set from seconds to hours (though practical use is typically minutes)
#### Timer Configuration Architecture
The command-line timer integrates with the existing timer system:
1. **Settings Integration**: Timer configuration is stored in `AppSettings.timer`
2. **Application Startup**: Timer is initialized during application startup if `--start-timer` is specified
3. **Message Bus Communication**: Timer expiration sends START_GAME message to trigger match progression
4. **Web Dashboard Integration**: Timer state is cached and served through API endpoints
#### Example Usage Scenarios
**Development Testing**:
```bash
# Quick 30-second timer for testing timer functionality
python main.py --start-timer 0.5 --no-qt
```
**Production Deployment**:
```bash
# 4-minute timer for match intervals
python main.py --start-timer 4 --web-port 8080
```
**Custom Timing**:
```bash
# 10-minute intervals for longer matches
python main.py --start-timer 10
```
### Match Timer API
#### Get Timer Configuration
......
......@@ -20,10 +20,19 @@ A cross-platform multimedia client application with video playback, web dashboar
- **Configuration Management**: Web-based configuration with section-based updates
- **Remote Shutdown**: Admin-only application shutdown from web dashboard
- **Overlay System**: Command-line switchable between WebEngine and native Qt overlay rendering
- **Automated Game Timer**: Configurable timer system that sends START_GAME messages after specified delay
## Recent Improvements
### Version 1.2.4 (August 2025)
### Version 1.2.6 (August 2025)
-**Automated Game Timer**: New `--start-timer` command line switch that sends START_GAME message after specified delay (default 4 minutes)
-**MatchTimerComponent Integration**: Fixed timer component initialization and registration with thread manager
-**Timer State API**: Real-time timer state API endpoints for web dashboard integration
-**Message Bus Communication**: Proper inter-thread communication between timer component and web dashboard
-**Timer Configuration**: Configurable timer delay with command-line parameter support
### Version 1.2.5 (August 2025)
-**Screen Casting Integration**: Complete screen capture and Chromecast streaming functionality integrated into MBetterC application
-**ScreenCastComponent**: New threaded component with HTTP server, FFmpeg screen capture, and Chromecast device management
......@@ -153,6 +162,12 @@ python main.py --overlay-type native
# Use WebEngine overlays (default)
python main.py --overlay-type webengine
# Start with automated game timer (4 minutes default)
python main.py --start-timer
# Start with custom timer delay (2 minutes)
python main.py --start-timer 2
# Show help
python main.py --help
```
......@@ -275,6 +290,11 @@ Threads communicate via Python Queues with structured messages:
- `POST /api/extraction/config` - Update game configuration settings
- `GET /api/server-time` - Get current server time for clock synchronization
#### Timer Management
- `GET /api/match-timer/state` - Get current timer state (running, remaining time, etc.)
- `POST /api/match-timer/control` - Control timer (start/stop) with action parameter
- `GET /api/match-timer/config` - Get timer configuration settings
### Message Types
#### Video Control
......@@ -298,6 +318,11 @@ Threads communicate via Python Queues with structured messages:
- `SYSTEM_STATUS` - Component status update
- `LOG_ENTRY` - Log entry for database storage
#### Timer Messages
- `START_GAME` - Start automated game timer with default interval
- `START_GAMES` - Start games with configuration-based interval
- `MATCH_START` - Start specific match with fixture and match IDs
#### Screen Cast Messages
- `WEB_ACTION` - Web dashboard screen cast control actions
- `SCREEN_CAST_START` - Start screen capture and streaming
......
......@@ -172,7 +172,15 @@ Examples:
action='version',
version='MbetterClient 1.0.0'
)
# Timer options
parser.add_argument(
'--start-timer',
type=int,
default=None,
help='Configure timer delay in minutes for START_GAME_DELAYED message when START_GAME is received (default: 4 minutes)'
)
return parser.parse_args()
def validate_arguments(args):
......@@ -217,6 +225,13 @@ def main():
settings.enable_qt = not args.no_qt
settings.enable_web = not args.no_web
settings.qt.use_native_overlay = args.native_overlay
# Timer settings
if args.start_timer is not None:
settings.timer.enabled = True
settings.timer.delay_minutes = args.start_timer
else:
settings.timer.enabled = False
# Screen cast settings
# Screen cast is enabled by default, disable only if --no-screen-cast is specified
......
......@@ -39,10 +39,15 @@ class MbetterClientApplication:
self.api_client = None
self.template_watcher = None
self.screen_cast = None
self.games_thread = None
self.match_timer = None
# Main loop thread
self._main_loop_thread: Optional[threading.Thread] = None
# Timer for automated game start
self._game_start_timer: Optional[threading.Timer] = None
logger.info("MbetterClient application initialized")
def initialize(self) -> bool:
......@@ -150,6 +155,7 @@ class MbetterClientApplication:
self.message_bus.subscribe("core", MessageType.SYSTEM_SHUTDOWN, self._handle_shutdown_message)
self.message_bus.subscribe("core", MessageType.CONFIG_UPDATE, self._handle_config_update)
self.message_bus.subscribe("core", MessageType.LOG_ENTRY, self._handle_log_entry)
self.message_bus.subscribe("core", MessageType.START_GAME, self._handle_start_game_message)
logger.info("Message bus initialized")
return True
......@@ -219,6 +225,20 @@ class MbetterClientApplication:
return False
else:
logger.info("Screen cast component disabled")
# Initialize games thread
if self._initialize_games_thread():
components_initialized += 1
else:
logger.error("Games thread initialization failed")
return False
# Initialize match timer
if self._initialize_match_timer():
components_initialized += 1
else:
logger.error("Match timer initialization failed")
return False
if components_initialized == 0:
logger.error("No components were initialized")
......@@ -342,31 +362,73 @@ class MbetterClientApplication:
"""Initialize screen cast component"""
try:
from .screen_cast import ScreenCastComponent
# Use configuration from settings
config = self.settings.screen_cast
# Set output directory to user data dir if not specified
output_dir = config.output_dir
if not output_dir:
output_dir = str(self.settings.get_user_data_dir() / "screen_cast")
self.screen_cast = ScreenCastComponent(
message_bus=self.message_bus,
stream_port=config.stream_port,
chromecast_name=config.chromecast_name,
output_dir=output_dir
)
# Register with thread manager
self.thread_manager.register_component("screen_cast", self.screen_cast)
logger.info(f"Screen cast component initialized on port {config.stream_port}")
return True
except Exception as e:
logger.error(f"Screen cast initialization failed: {e}")
return False
def _initialize_games_thread(self) -> bool:
"""Initialize games thread component"""
try:
from .games_thread import GamesThread
self.games_thread = GamesThread(
name="games_thread",
message_bus=self.message_bus,
db_manager=self.db_manager
)
# Register with thread manager
self.thread_manager.register_component("games_thread", self.games_thread)
logger.info("Games thread component initialized")
return True
except Exception as e:
logger.error(f"Games thread initialization failed: {e}")
return False
def _initialize_match_timer(self) -> bool:
"""Initialize match timer component"""
try:
from .match_timer import MatchTimerComponent
self.match_timer = MatchTimerComponent(
message_bus=self.message_bus,
db_manager=self.db_manager,
config_manager=self.config_manager
)
# Register with thread manager
self.thread_manager.register_component("match_timer", self.match_timer)
logger.info("Match timer component initialized")
return True
except Exception as e:
logger.error(f"Match timer initialization failed: {e}")
return False
def run(self) -> int:
"""Run the application"""
......@@ -408,7 +470,7 @@ class MbetterClientApplication:
}
)
self.message_bus.publish(ready_message, broadcast=True)
logger.info("MbetterClient application started successfully")
# If Qt player is enabled, run its event loop on the main thread
......@@ -612,7 +674,7 @@ class MbetterClientApplication:
level = message.data.get("level", "INFO")
log_message = message.data.get("message", "")
details = message.data.get("details", {})
# Add to database log
self.db_manager.add_log_entry(
level=level,
......@@ -620,15 +682,69 @@ class MbetterClientApplication:
message=log_message,
details=details
)
except Exception as e:
logger.error(f"Failed to handle log entry: {e}")
def _handle_start_game_message(self, message: Message):
"""Handle START_GAME message by starting the timer"""
try:
if not self.settings.timer.enabled:
logger.debug("Timer not enabled, ignoring START_GAME message")
return
# Cancel any existing timer
self._cancel_game_timer()
# Start new timer
delay_seconds = self.settings.timer.delay_minutes * 60
logger.info(f"START_GAME message received from {message.sender}, starting timer for {self.settings.timer.delay_minutes} minutes ({delay_seconds} seconds)")
self._game_start_timer()
except Exception as e:
logger.error(f"Failed to handle START_GAME message: {e}")
def _run_additional_tasks(self):
"""Placeholder for additional periodic tasks"""
# TODO: Implement additional tasks as requested by user
# This is where future extensions can be added
pass
def _start_game_timer(self):
"""Start the timer for automated game start"""
if not self.settings.timer.enabled:
return
delay_seconds = self.settings.timer.delay_minutes * 60
logger.info(f"Starting game timer: {self.settings.timer.delay_minutes} minutes ({delay_seconds} seconds)")
self._game_start_timer = threading.Timer(delay_seconds, self._on_game_timer_expired)
self._game_start_timer.daemon = True
self._game_start_timer.start()
def _on_game_timer_expired(self):
"""Called when the game start timer expires"""
logger.info("Game start timer expired, sending START_GAME_DELAYED message")
try:
# Create and send START_GAME_DELAYED message
start_game_delayed_message = MessageBuilder.start_game_delayed(
sender="timer",
delay_minutes=self.settings.timer.delay_minutes
)
self.message_bus.publish(start_game_delayed_message, broadcast=True)
logger.info("START_GAME_DELAYED message sent successfully")
except Exception as e:
logger.error(f"Failed to send START_GAME_DELAYED message: {e}")
def _cancel_game_timer(self):
"""Cancel the game start timer if it's running"""
if self._game_start_timer and self._game_start_timer.is_alive():
logger.info("Cancelling game start timer")
self._game_start_timer.cancel()
self._game_start_timer = None
def _check_component_health(self):
"""Check health of all components"""
......@@ -662,10 +778,13 @@ class MbetterClientApplication:
def shutdown(self):
"""Shutdown the application"""
logger.info("Application shutdown requested")
self.running = False
self.shutdown_event.set()
# Cancel game timer if running
self._cancel_game_timer()
# Send shutdown message to all components
if self.message_bus:
shutdown_message = Message(
......@@ -674,7 +793,7 @@ class MbetterClientApplication:
data={"reason": "Application shutdown"}
)
self.message_bus.publish(shutdown_message, broadcast=True)
# If Qt player is running, quit the QApplication
if self.qt_player and hasattr(self.qt_player, 'app') and self.qt_player.app:
try:
......
This diff is collapsed.
......@@ -63,6 +63,7 @@ class MessageType(Enum):
# Game messages
START_GAME = "START_GAME"
START_GAMES = "START_GAMES"
START_GAME_DELAYED = "START_GAME_DELAYED"
MATCH_START = "MATCH_START"
GAME_STATUS = "GAME_STATUS"
GAME_UPDATE = "GAME_UPDATE"
......@@ -549,6 +550,18 @@ class MessageBuilder:
}
)
@staticmethod
def start_game_delayed(sender: str, fixture_id: Optional[str] = None, delay_minutes: Optional[int] = None) -> Message:
"""Create START_GAME_DELAYED message"""
return Message(
type=MessageType.START_GAME_DELAYED,
sender=sender,
data={
"fixture_id": fixture_id,
"delay_minutes": delay_minutes
}
)
@staticmethod
def match_start(sender: str, fixture_id: str, match_id: int) -> Message:
"""Create MATCH_START message"""
......
......@@ -41,6 +41,16 @@ class WebDashboard(ThreadedComponent):
self.auth_manager: Optional[AuthManager] = None
self.api: Optional[DashboardAPI] = None
self.main_application = None # Will be set by the main application
# Timer state storage
self.current_timer_state: Dict[str, Any] = {
"running": False,
"remaining_seconds": 0,
"total_seconds": 0,
"fixture_id": None,
"match_id": None,
"start_time": None
}
# Register message queue
self.message_queue = self.message_bus.register_component(self.name)
......@@ -72,6 +82,7 @@ class WebDashboard(ThreadedComponent):
# Subscribe to messages
self.message_bus.subscribe(self.name, MessageType.CONFIG_UPDATE, self._handle_config_update)
self.message_bus.subscribe(self.name, MessageType.SYSTEM_STATUS, self._handle_system_status)
self.message_bus.subscribe(self.name, MessageType.CUSTOM, self._handle_custom_message)
logger.info("WebDashboard initialized successfully")
return True
......@@ -312,14 +323,40 @@ class WebDashboard(ThreadedComponent):
try:
status = message.data.get("status")
sender = message.sender
logger.debug(f"System status from {sender}: {status}")
# Store status for web interface
# This could be cached or stored in database for display
except Exception as e:
logger.error(f"Failed to handle system status: {e}")
def _handle_custom_message(self, message: Message):
"""Handle custom messages (like timer state responses)"""
try:
response = message.data.get("response")
if response == "timer_state":
# Update stored timer state
timer_state = message.data.get("timer_state", {})
self.current_timer_state.update(timer_state)
logger.debug(f"Timer state updated: {timer_state}")
elif response == "timer_stopped":
# Reset timer state
self.current_timer_state = {
"running": False,
"remaining_seconds": 0,
"total_seconds": 0,
"fixture_id": None,
"match_id": None,
"start_time": None
}
logger.debug("Timer stopped")
except Exception as e:
logger.error(f"Failed to handle custom message: {e}")
def get_app_context(self):
"""Get Flask application context"""
......@@ -381,7 +418,7 @@ class WebDashboard(ThreadedComponent):
data={"section": "status"}
)
self.message_bus.publish(status_request)
# For now, return basic status
# In a full implementation, this would wait for response or cache status
return {
......@@ -390,11 +427,43 @@ class WebDashboard(ThreadedComponent):
"port": self.settings.port,
"timestamp": time.time()
}
except Exception as e:
logger.error(f"Failed to get system status: {e}")
return {"error": str(e)}
def get_timer_state(self) -> Dict[str, Any]:
"""Get current timer state"""
try:
# Request fresh timer state from match_timer component
if self.message_bus:
request_message = Message(
type=MessageType.CUSTOM,
sender=self.name,
recipient="match_timer",
data={
"request": "get_timer_state",
"timestamp": time.time()
},
correlation_id=f"timer_state_{int(time.time() * 1000)}"
)
self.message_bus.publish(request_message)
# Return current cached state
return self.current_timer_state.copy()
except Exception as e:
logger.error(f"Failed to get timer state: {e}")
return {
"running": False,
"remaining_seconds": 0,
"total_seconds": 0,
"fixture_id": None,
"match_id": None,
"start_time": None,
"error": str(e)
}
def create_app(db_manager: DatabaseManager, config_manager: ConfigManager,
settings: WebConfig) -> Flask:
......
......@@ -3,6 +3,7 @@ Flask routes for web dashboard
"""
import logging
import time
from datetime import datetime
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, flash, session
from flask_login import login_required, current_user, login_user, logout_user
......@@ -1975,4 +1976,195 @@ def update_extraction_config():
except Exception as e:
logger.error(f"API update extraction config error: {e}")
return jsonify({"error": str(e)}), 500
# Betting Mode API routes
@api_bp.route('/betting-mode')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
def get_betting_mode():
"""Get current user's betting mode"""
try:
from ..database.models import BettingModeModel
user_id = getattr(current_user, 'id', None) or getattr(request, 'current_user', {}).get('user_id')
if not user_id:
return jsonify({"error": "User not authenticated"}), 401
session = api_bp.db_manager.get_session()
try:
# Get user's betting mode
betting_mode = session.query(BettingModeModel).filter_by(user_id=user_id).first()
if betting_mode:
mode = betting_mode.mode
else:
# Return default if not set
mode = 'all_bets_on_start'
return jsonify({
"success": True,
"betting_mode": mode,
"is_default": betting_mode is None
})
finally:
session.close()
except Exception as e:
logger.error(f"API get betting mode error: {e}")
return jsonify({"error": str(e)}), 500
@api_bp.route('/betting-mode', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
def set_betting_mode():
"""Set current user's betting mode"""
try:
from ..database.models import BettingModeModel
user_id = getattr(current_user, 'id', None) or getattr(request, 'current_user', {}).get('user_id')
if not user_id:
return jsonify({"error": "User not authenticated"}), 401
data = request.get_json() or {}
mode = data.get('betting_mode')
if not mode:
return jsonify({"error": "betting_mode is required"}), 400
# Validate mode
valid_modes = ['all_bets_on_start', 'one_bet_at_a_time']
if mode not in valid_modes:
return jsonify({"error": f"Invalid betting mode. Must be one of: {', '.join(valid_modes)}"}), 400
session = api_bp.db_manager.get_session()
try:
# Check if betting mode already exists for user
existing_mode = session.query(BettingModeModel).filter_by(user_id=user_id).first()
if existing_mode:
# Update existing mode
existing_mode.mode = mode
existing_mode.updated_at = datetime.utcnow()
else:
# Create new betting mode
new_mode = BettingModeModel(
user_id=user_id,
mode=mode
)
session.add(new_mode)
session.commit()
logger.info(f"Updated betting mode for user {user_id}: {mode}")
return jsonify({
"success": True,
"message": f"Betting mode set to '{mode}'",
"betting_mode": mode
})
finally:
session.close()
except Exception as e:
logger.error(f"API set betting mode error: {e}")
return jsonify({"error": str(e)}), 500
# Server-side Match Timer API endpoints
@api_bp.route('/match-timer/state')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
def get_match_timer_state():
"""Get current match timer state from server-side component"""
try:
# Get timer state from web dashboard (which caches responses from match_timer)
if hasattr(g, 'main_app') and g.main_app and hasattr(g.main_app, 'web_dashboard'):
timer_state = g.main_app.web_dashboard.get_timer_state()
return jsonify({
"success": True,
"running": timer_state.get("running", False),
"remaining_seconds": timer_state.get("remaining_seconds", 0),
"total_seconds": timer_state.get("total_seconds", 0),
"fixture_id": timer_state.get("fixture_id"),
"match_id": timer_state.get("match_id"),
"start_time": timer_state.get("start_time"),
"elapsed_seconds": timer_state.get("elapsed_seconds", 0)
})
else:
return jsonify({
"success": False,
"error": "Web dashboard not available"
}), 500
except Exception as e:
logger.error(f"API get match timer state error: {e}")
return jsonify({"error": str(e)}), 500
@api_bp.route('/match-timer/control', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
def control_match_timer():
"""Control the match timer (admin only)"""
try:
data = request.get_json() or {}
action = data.get('action')
if not action:
return jsonify({"error": "Action is required"}), 400
if action == 'start':
# Send START_GAMES message to start the timer
if api_bp.message_bus:
from ..core.message_bus import MessageBuilder, MessageType
start_message = MessageBuilder.start_games(sender="web_dashboard")
api_bp.message_bus.publish(start_message)
return jsonify({
"success": True,
"message": "Match timer started"
})
else:
return jsonify({
"success": False,
"error": "Message bus not available"
}), 500
elif action == 'stop':
# Send custom stop message to timer component
if api_bp.message_bus:
from ..core.message_bus import Message, MessageType
stop_message = Message(
type=MessageType.CUSTOM,
sender="web_dashboard",
recipient="match_timer",
data={
"request": "stop",
"timestamp": time.time()
}
)
api_bp.message_bus.publish(stop_message)
return jsonify({
"success": True,
"message": "Match timer stop requested"
})
else:
return jsonify({
"success": False,
"error": "Message bus not available"
}), 500
else:
return jsonify({"error": f"Unknown action: {action}"}), 400
except Exception as e:
logger.error(f"API control match timer error: {e}")
return jsonify({"error": str(e)}), 500
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment