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 %}
+
+ {% for category, message in messages %}
+ - {{ message }}
+ {% endfor %}
+
+ {% endif %}
+ {% endwith %}
+
+
+
+
+
+
+
+
+
+
+
Display Schedule
+
Set the time for the display to be active. A restart is needed for changes to take effect.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 %}
-
- {% for category, message in messages %}
- - {{ message }}
- {% endfor %}
-
- {% endif %}
- {% endwith %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-"""
-
@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