From 9bb4f088a651c7a1bdf78e42329312bc71301993 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 8 Jul 2025 19:33:08 -0500 Subject: [PATCH] adding scheduling function to web ui and display --- config/config.json | 5 + src/display_controller.py | 62 +++++- templates/index.html | 189 ++++++++++++++++ web_interface.py | 450 +++++++------------------------------- 4 files changed, 326 insertions(+), 380 deletions(-) create mode 100644 templates/index.html diff --git a/config/config.json b/config/config.json index f289401d..cc46e04d 100644 --- a/config/config.json +++ b/config/config.json @@ -1,5 +1,10 @@ { "web_display_autostart": false, + "schedule": { + "enabled": false, + "start_time": "07:00", + "end_time": "23:00" + }, "timezone": "America/Chicago", "location": { "city": "Dallas", diff --git a/src/display_controller.py b/src/display_controller.py index 93836bde..a0cd345b 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -2,6 +2,7 @@ import time import logging import sys from typing import Dict, Any, List +from datetime import datetime, time as time_obj # Configure logging logging.basicConfig( @@ -411,6 +412,12 @@ class DisplayController: logger.info(f"Initial display mode: {self.current_display_mode}") logger.info("DisplayController initialized with display_manager: %s", id(self.display_manager)) + # --- SCHEDULING & CONFIG REFRESH --- + self.config_check_interval = 30 + self.last_config_check = 0 + self.is_display_active = True + self._load_config() # Initial load of schedule + def _handle_music_update(self, track_info: Dict[str, Any], significant_change: bool = False): """Callback for when music track info changes.""" # MusicManager now handles its own display state (album art, etc.) @@ -712,6 +719,44 @@ class DisplayController: self.ncaa_fb_current_team_index = (self.ncaa_fb_current_team_index + 1) % len(self.ncaa_fb_favorite_teams) self.ncaa_fb_showing_recent = True # Reset to recent for the new team + # --- SCHEDULING METHODS --- + def _load_config(self): + """Load configuration from the config manager and parse schedule settings.""" + self.config = self.config_manager.load_config() + schedule_config = self.config.get('schedule', {}) + self.schedule_enabled = schedule_config.get('enabled', False) + try: + self.start_time = datetime.strptime(schedule_config.get('start_time', '07:00'), '%H:%M').time() + self.end_time = datetime.strptime(schedule_config.get('end_time', '22:00'), '%H:%M').time() + except (ValueError, TypeError): + logger.warning("Invalid time format in schedule config. Using defaults.") + self.start_time = time_obj(7, 0) + self.end_time = time_obj(22, 0) + + def _check_schedule(self): + """Check if the display should be active based on the schedule.""" + if not self.schedule_enabled: + if not self.is_display_active: + logger.info("Schedule is disabled. Activating display.") + self.is_display_active = True + return + + now_time = datetime.now().time() + + # Handle overnight schedules + if self.start_time <= self.end_time: + should_be_active = self.start_time <= now_time < self.end_time + else: + should_be_active = now_time >= self.start_time or now_time < self.end_time + + if should_be_active and not self.is_display_active: + logger.info("Within scheduled time. Activating display.") + self.is_display_active = True + elif not should_be_active and self.is_display_active: + logger.info("Outside of scheduled time. Deactivating display.") + self.display_manager.clear() + self.is_display_active = False + def run(self): """Run the display controller, switching between displays.""" if not self.available_modes: @@ -722,6 +767,17 @@ class DisplayController: try: while True: current_time = time.time() + + # Periodically check for config changes + if current_time - self.last_config_check > self.config_check_interval: + self._load_config() + self.last_config_check = current_time + + # Enforce the schedule + self._check_schedule() + if not self.is_display_active: + time.sleep(60) + continue # Update data for all modules first self._update_modules() @@ -1024,10 +1080,8 @@ class DisplayController: # Force clear on the next iteration after an error to be safe self.force_clear = True - - # Small sleep removed - updates/drawing should manage timing - # time.sleep(self.update_interval) - #time.sleep(self.update_interval) # Re-add the sleep + # Add a short sleep to prevent high CPU usage + time.sleep(0.1) except KeyboardInterrupt: logger.info("Display controller stopped by user") diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 00000000..28c6e8d4 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,189 @@ + + + + + LED Matrix Config + + + +
+

LED Matrix Configuration

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + + {% endif %} + {% endwith %} + +
+ + + + +
+ + +
+

Display Schedule

+

Set the time for the display to be active. A restart is needed for changes to take effect.

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ +

Main Configuration ({{ main_config_path }})

+ + +
+
+ + +
+
+ +

Secrets Configuration ({{ secrets_config_path }})

+ + +
+
+ + +
+

System Actions

+

Control the display service and system.

+
+ + +
+ + +
+ +
+ +
+
+

Action Output:

+
No action run yet.
+
+
+
+ + + + \ No newline at end of file diff --git a/web_interface.py b/web_interface.py index ebf9577d..d4156fc1 100644 --- a/web_interface.py +++ b/web_interface.py @@ -1,410 +1,108 @@ -from flask import Flask, render_template_string, request, redirect, url_for, flash, jsonify +from flask import Flask, render_template, request, redirect, url_for, flash, jsonify import json -import os # Added os import -import subprocess # Added subprocess import +import os +import subprocess from src.config_manager import ConfigManager app = Flask(__name__) -app.secret_key = os.urandom(24) # Needed for flash messages +app.secret_key = os.urandom(24) config_manager = ConfigManager() -CONFIG_PAGE_TEMPLATE = """ - - - - - LED Matrix Config - - - - - - - - -
-

LED Matrix Configuration

- - {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - - {% endif %} - {% endwith %} - -
- - - -
- -
- - -
-

Main Configuration ({{ main_config_path }})

- - -
- -
-

Secrets Configuration ({{ secrets_config_path }})

- - -
- -
-

Display & Service Actions

-
- - -
- - -
- -
- -
-
-

Action Output:

-
No action run yet.
-
-
- - -
-
- - - - - - - - - - - - -""" - @app.route('/') -def display_config_route(): - active_tab = request.args.get('tab', 'main') # Default to 'main' tab - main_config_data = {} - secrets_config_data = {} - error_message = None - +def index(): try: + main_config = config_manager.load_config() + schedule_config = main_config.get('schedule', {}) + main_config_data = config_manager.get_raw_file_content('main') - except Exception as e: - flash(f"Error loading main config: {str(e)}", "error") - - try: secrets_config_data = config_manager.get_raw_file_content('secrets') + main_config_json = json.dumps(main_config_data, indent=4) + secrets_config_json = json.dumps(secrets_config_data, indent=4) + except Exception as e: - flash(f"Error loading secrets config: {str(e)}", "error") + flash(f"Error loading configuration: {e}", "error") + schedule_config = {} + main_config_json = "{}" + secrets_config_json = "{}" - main_config_json = json.dumps(main_config_data, indent=4) - secrets_config_json = json.dumps(secrets_config_data, indent=4) - - return render_template_string(CONFIG_PAGE_TEMPLATE, - main_config_json=main_config_json, - secrets_config_json=secrets_config_json, - active_tab=active_tab, - main_config_path=config_manager.get_config_path(), - secrets_config_path=config_manager.get_secrets_path()) - -@app.route('/save', methods=['POST']) -def save_config_route(): - config_type = request.form.get('config_type', 'main') - data_to_save_str = "" - - if config_type == 'main': - data_to_save_str = request.form['main_config_data'] - elif config_type == 'secrets': - data_to_save_str = request.form['secrets_config_data'] - else: - flash("Invalid configuration type specified for saving.", "error") - return redirect(url_for('display_config_route')) + return render_template('index.html', + schedule_config=schedule_config, + main_config_json=main_config_json, + secrets_config_json=secrets_config_json, + main_config_path=config_manager.get_config_path(), + secrets_config_path=config_manager.get_secrets_path()) +@app.route('/save_schedule', methods=['POST']) +def save_schedule_route(): try: - new_data = json.loads(data_to_save_str) + main_config = config_manager.load_config() + + schedule_data = { + 'enabled': 'schedule_enabled' in request.form, + 'start_time': request.form.get('start_time', '07:00'), + 'end_time': request.form.get('end_time', '22:00') + } + + main_config['schedule'] = schedule_data + config_manager.save_config(main_config) + + flash("Schedule updated successfully! Restart the display for changes to take effect.", "success") + + except Exception as e: + flash(f"Error saving schedule: {e}", "error") + + return redirect(url_for('index')) + +@app.route('/save_config', methods=['POST']) +def save_config_route(): + config_type = request.form.get('config_type') + config_data_str = request.form.get('config_data') + + try: + new_data = json.loads(config_data_str) config_manager.save_raw_file_content(config_type, new_data) flash(f"{config_type.capitalize()} configuration saved successfully!", "success") except json.JSONDecodeError: - flash(f"Error: Invalid JSON format submitted for {config_type} config.", "error") + flash(f"Error: Invalid JSON format for {config_type} config.", "error") except Exception as e: - flash(f"Error saving {config_type} configuration: {str(e)}", "error") - - return redirect(url_for('display_config_route', tab=config_type)) # Redirect back to the same tab + flash(f"Error saving {config_type} configuration: {e}", "error") + + return redirect(url_for('index')) @app.route('/run_action', methods=['POST']) def run_action_route(): data = request.get_json() action = data.get('action') - command_parts = [] # Use a list for subprocess - explanation_msg = "" + + commands = { + 'start_display': ["sudo", "python3", "display_controller.py"], + 'stop_display': ["sudo", "pkill", "-f", "display_controller.py"], + 'enable_autostart': ["sudo", "systemctl", "enable", "ledmatrix.service"], + 'disable_autostart': ["sudo", "systemctl", "disable", "ledmatrix.service"], + 'reboot_system': ["sudo", "reboot"], + 'git_pull': ["git", "pull"] + } - if action == 'start_display': - command_parts = ["sudo", "python", "display_controller.py"] # Changed command - explanation_msg = "Starts the LED matrix display by directly running display_controller.py with sudo." - elif action == 'stop_display': - command_parts = ["bash", "stop_display.sh"] - explanation_msg = "Stops the LED matrix display by executing the stop_display.sh script." - elif action == 'enable_autostart': - command_parts = ["sudo", "systemctl", "enable", "ledmatrix.service"] - explanation_msg = "Enables the LED matrix service to start automatically on boot." - elif action == 'disable_autostart': - command_parts = ["sudo", "systemctl", "disable", "ledmatrix.service"] - explanation_msg = "Disables the LED matrix service from starting automatically on boot." - elif action == 'reboot_system': - command_parts = ["sudo", "reboot"] - explanation_msg = "Reboots the system." - elif action == 'git_pull': - command_parts = ["git", "pull"] - explanation_msg = "Downloads the latest updates from the repository." - else: - return jsonify({ - "status": "error", - "message": "Invalid action specified.", - "error": "Unknown action" - }), 400 + command_parts = commands.get(action) + + if not command_parts: + return jsonify({"status": "error", "message": "Invalid action."}), 400 try: - # Direct execution using subprocess - process_result = subprocess.run(command_parts, capture_output=True, text=True, check=False) + result = subprocess.run(command_parts, capture_output=True, text=True, check=False) + + status = "success" if result.returncode == 0 else "error" + message = f"Action '{action}' completed." - stdout_content = process_result.stdout - stderr_content = process_result.stderr - exit_code = process_result.returncode - - current_status = "success" - message_to_user = f"Action '{action}' executed. Exit code: {exit_code}." - - if exit_code != 0: - current_status = "error" - message_to_user = f"Action '{action}' failed. Exit code: {exit_code}." - elif stderr_content: # Even with exit code 0, stderr might contain warnings - current_status = "warning" - message_to_user = f"Action '{action}' executed with output in stderr (Exit code: {exit_code})." - return jsonify({ - "status": current_status, - "message": message_to_user, - "stdout": stdout_content, - "stderr": stderr_content + "status": status, + "message": message, + "stdout": result.stdout, + "stderr": result.stderr }) - except FileNotFoundError as e: - # This occurs if the command itself (e.g., 'bash', 'sudo', or the script) isn't found - return jsonify({ - "status": "error", - "message": f"Error executing action '{action}': Command or script not found.", - "error": str(e), - "stdout": "", - "stderr": f"Command not found: {command_parts[0] if command_parts else ''}" - }), 500 except Exception as e: - # Catch any other exceptions during subprocess execution or in this route - return jsonify({ - "status": "error", - "message": f"An unexpected error occurred while processing action: {action}", - "error": str(e) - }), 500 + return jsonify({"status": "error", "message": str(e)}), 500 if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=5000) \ No newline at end of file