diff --git a/WEB_INTERFACE_README.md b/WEB_INTERFACE_README.md new file mode 100644 index 00000000..356d1996 --- /dev/null +++ b/WEB_INTERFACE_README.md @@ -0,0 +1,218 @@ +# LED Matrix Web Interface + +A user-friendly web interface for configuring the LED Matrix display system. This interface replaces raw JSON editing with intuitive forms, toggles, and dropdowns to prevent configuration errors. + +## Features + +### 🎛️ **Form-Based Configuration** +- **Toggles**: Easy on/off switches for enabling features +- **Dropdowns**: Predefined options for hardware settings +- **Input Fields**: Validated text and number inputs +- **Descriptions**: Helpful tooltips explaining each setting + +### 📱 **Organized Tabs** +1. **Schedule**: Set display on/off times +2. **Display Settings**: Hardware configuration (rows, columns, brightness, etc.) +3. **Sports**: Configure favorite teams for MLB, NFL, NBA +4. **Weather**: Location and weather display settings +5. **Stocks & Crypto**: Stock symbols and cryptocurrency settings +6. **Music**: Music source configuration (YouTube Music, Spotify) +7. **Calendar**: Google Calendar integration settings +8. **API Keys**: Secure storage for service API keys +9. **Actions**: System control (start/stop display, reboot, etc.) + +### 🔒 **Security Features** +- Password fields for API keys +- Secure form submission +- Input validation +- Error handling with user-friendly messages + +### 🎨 **Modern UI** +- Responsive design +- Clean, professional appearance +- Intuitive navigation +- Visual feedback for actions + +## Getting Started + +### Prerequisites +- Python 3.7+ +- Flask +- LED Matrix system running on Raspberry Pi + +### Installation + +1. **Install Dependencies** + ```bash + pip install flask requests + ``` + +2. **Start the Web Interface** + ```bash + python3 web_interface.py + ``` + +3. **Access the Interface** + - Open a web browser + - Navigate to: `http://[PI_IP_ADDRESS]:5000` + - Example: `http://192.168.1.100:5000` + +## Configuration Guide + +### Schedule Tab +Configure when the display should be active: +- **Enable Schedule**: Toggle to turn automatic scheduling on/off +- **Display On Time**: When the display should turn on (24-hour format) +- **Display Off Time**: When the display should turn off (24-hour format) + +### Display Settings Tab +Configure the LED matrix hardware: +- **Rows**: Number of LED rows (typically 32) +- **Columns**: Number of LED columns (typically 64) +- **Chain Length**: Number of LED panels chained together +- **Parallel**: Number of parallel chains +- **Brightness**: LED brightness (1-100) +- **Hardware Mapping**: Type of LED matrix hardware +- **GPIO Slowdown**: GPIO slowdown factor (0-5) + +### Sports Tab +Configure sports team preferences: +- **Enable Leagues**: Toggle MLB, NFL, NBA on/off +- **Favorite Teams**: Enter team abbreviations (e.g., "TB, DAL") +- **Team Examples**: + - MLB: TB (Tampa Bay), TEX (Texas) + - NFL: TB (Tampa Bay), DAL (Dallas) + - NBA: DAL (Dallas), BOS (Boston) + +### Weather Tab +Configure weather display settings: +- **Enable Weather**: Toggle weather display on/off +- **City**: Your city name +- **State**: Your state/province +- **Units**: Fahrenheit or Celsius +- **Update Interval**: How often to update weather data (seconds) + +### Stocks & Crypto Tab +Configure financial data display: +- **Enable Stocks**: Toggle stock display on/off +- **Stock Symbols**: Enter symbols (e.g., "AAPL, GOOGL, MSFT") +- **Enable Crypto**: Toggle cryptocurrency display on/off +- **Crypto Symbols**: Enter symbols (e.g., "BTC-USD, ETH-USD") +- **Update Interval**: How often to update data (seconds) + +### Music Tab +Configure music display settings: +- **Enable Music Display**: Toggle music display on/off +- **Preferred Source**: YouTube Music or Spotify +- **YouTube Music Companion URL**: URL for YTM companion app +- **Polling Interval**: How often to check for music updates (seconds) + +### Calendar Tab +Configure Google Calendar integration: +- **Enable Calendar**: Toggle calendar display on/off +- **Max Events**: Maximum number of events to display +- **Update Interval**: How often to update calendar data (seconds) +- **Calendars**: Comma-separated calendar names + +### API Keys Tab +Securely store API keys for various services: +- **Weather API**: OpenWeatherMap API key +- **YouTube API**: YouTube API key and channel ID +- **Spotify API**: Client ID, Client Secret, and Redirect URI + +### Actions Tab +Control the LED Matrix system: +- **Start Display**: Start the LED display service +- **Stop Display**: Stop the LED display service +- **Enable Auto-Start**: Enable automatic startup on boot +- **Disable Auto-Start**: Disable automatic startup +- **Reboot System**: Restart the Raspberry Pi +- **Download Latest Update**: Pull latest code from Git + +## API Keys Setup + +### OpenWeatherMap API +1. Go to [OpenWeatherMap](https://openweathermap.org/api) +2. Sign up for a free account +3. Get your API key +4. Enter it in the Weather API section + +### YouTube API +1. Go to [Google Cloud Console](https://console.developers.google.com/) +2. Create a new project +3. Enable YouTube Data API v3 +4. Create credentials (API key) +5. Enter the API key and your channel ID + +### Spotify API +1. Go to [Spotify Developer Dashboard](https://developer.spotify.com/dashboard) +2. Create a new app +3. Get your Client ID and Client Secret +4. Set the Redirect URI to: `http://127.0.0.1:8888/callback` + +## Testing + +Run the test script to verify the web interface is working: + +```bash +python3 test_web_interface.py +``` + +## Troubleshooting + +### Common Issues + +1. **Web interface not accessible** + - Check if the service is running: `python3 web_interface.py` + - Verify the IP address and port + - Check firewall settings + +2. **Configuration not saving** + - Check file permissions on config files + - Verify JSON syntax in logs + - Ensure config directory exists + +3. **Actions not working** + - Check if running on Raspberry Pi + - Verify sudo permissions + - Check system service status + +### Error Messages + +- **"Invalid JSON format"**: Check the configuration syntax +- **"Permission denied"**: Run with appropriate permissions +- **"Connection refused"**: Check if the service is running + +## File Structure + +``` +LEDMatrix/ +├── web_interface.py # Main Flask application +├── templates/ +│ └── index.html # Web interface template +├── config/ +│ ├── config.json # Main configuration +│ └── config_secrets.json # API keys (secure) +└── test_web_interface.py # Test script +``` + +## Security Notes + +- API keys are stored securely in `config_secrets.json` +- The web interface runs on port 5000 by default +- Consider using HTTPS in production +- Regularly update API keys and credentials + +## Contributing + +When adding new configuration options: + +1. Update the HTML template with appropriate form fields +2. Add JavaScript handlers for form submission +3. Update the Flask backend to handle new fields +4. Add validation and error handling +5. Update this documentation + +## License + +This project is part of the LED Matrix display system. \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 28c6e8d4..379481dc 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,34 +4,142 @@ LED Matrix Config @@ -77,71 +262,486 @@
- - + + + + + + +
-

Display Schedule

-

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

-
-
- - -
-
- - -
-
- - -
- -
+
+

Display Schedule

+

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

+
+
+ +
+ + Turn display on/off automatically +
+
+
+ + +
Time when the display should turn on
+
+
+ + +
Time when the display should turn off
+
+ +
+
- -
-
- -

Main Configuration ({{ main_config_path }})

- - -
+ +
+
+

Display Hardware Settings

+
+ + + +
+
+
+ + +
Number of LED rows
+
+
+ + +
Number of LED columns
+
+
+ + +
Number of LED panels chained together
+
+
+ + +
Number of parallel chains
+
+
+
+
+ + +
LED brightness (1-100)
+
+
+ + +
Hardware mapping type
+
+
+ + +
GPIO slowdown factor (0-5)
+
+
+
+ + +
+
+
+ + +
+
+

Sports Configuration

+

Configure which sports leagues to display and their settings.

+ +
+

MLB (Baseball)

+
+ +
+ +
+
+
+ +
+ + +
+
Comma-separated team abbreviations (e.g., TB, TEX)
+
+
+ +
+

NFL (Football)

+
+ +
+ +
+
+
+ +
+ + +
+
Comma-separated team abbreviations
+
+
+ +
+

NBA (Basketball)

+
+ +
+ +
+
+
+ +
+ + +
+
Comma-separated team abbreviations
+
+
+ + +
+
+ + +
+
+

Weather Configuration

+
+ + + +
+ +
+ +
+
+ +
+ + +
City name for weather data
+
+ +
+ + +
State/province name
+
+ +
+ + +
Temperature units
+
+ +
+ + +
How often to update weather data (300-3600 seconds)
+
+ + +
+
+
+ + +
+
+

Stocks & Crypto Configuration

+ +
+

Stocks

+
+ + + +
+ +
+ +
+
+ +
+ +
+ + +
+
Comma-separated stock symbols
+
+ +
+ + +
How often to update stock data
+
+ + +
+
+ +
+

Cryptocurrency

+
+ + + +
+ +
+ +
+
+ +
+ +
+ + +
+
Comma-separated crypto symbols (e.g., BTC-USD, ETH-USD)
+
+ +
+ + +
How often to update crypto data
+
+ + +
+
+
+
+ + +
+
+

Music Configuration

+
+ + + +
+ +
+ +
+
+ +
+ + +
Primary music source to display
+
+ +
+ + +
URL for YouTube Music companion app
+
+ +
+ + +
How often to check for music updates
+
+ + +
+
+
+ + +
+
+

Calendar Configuration

+
+ + + +
+ +
+ +
+
+ +
+ + +
Maximum number of events to display
+
+ +
+ + +
How often to update calendar data
+
+ +
+ +
+ + +
+
Comma-separated calendar names
+
+ + +
+
-
- -

Secrets Configuration ({{ secrets_config_path }})

- - -
+
+

API Keys Configuration

+

Enter your API keys for various services. These are stored securely and not shared.

+ + + +
+
+ + + +
+

Weather API

+
+ + +
Get your free API key from OpenWeatherMap
+
+
+ +
+

YouTube API

+
+ + +
Get your API key from Google Cloud Console
+
+
+ + +
Your YouTube channel ID (found in channel settings)
+
+
+ +
+

Spotify API

+
+ + + +
+
+ + +
Your Spotify Client Secret
+
+
+ + +
Redirect URI for Spotify authentication
+
+
+ + +
+
+ +
+
+ +

Secrets Configuration ({{ secrets_config_path }})

+ + +
+
+
-

System Actions

-

Control the display service and system.

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

Action Output:

-
No action run yet.
-
+
+

System Actions

+

Control the display service and system.

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

Action Output:

+
No action run yet.
+
+
@@ -160,6 +760,329 @@ evt.currentTarget.className += " active"; } + function toggleJsonEditor(section) { + const formEditor = document.getElementById(section + '-form'); + const jsonEditor = document.getElementById(section + '-json'); + + if (formEditor.style.display === 'none') { + formEditor.style.display = 'block'; + jsonEditor.style.display = 'none'; + } else { + formEditor.style.display = 'none'; + jsonEditor.style.display = 'block'; + } + } + + function addArrayItem(inputId) { + const input = document.getElementById(inputId); + const currentValue = input.value.trim(); + if (currentValue && !currentValue.endsWith(',')) { + input.value = currentValue + ', '; + } + input.focus(); + } + + function saveSportsConfig() { + // Collect all sports configuration and save + const config = { + mlb: { + enabled: document.getElementById('mlb_enabled').checked, + favorite_teams: document.getElementById('mlb_favorites').value.split(',').map(s => s.trim()).filter(s => s) + }, + nfl_scoreboard: { + enabled: document.getElementById('nfl_enabled').checked, + favorite_teams: document.getElementById('nfl_favorites').value.split(',').map(s => s.trim()).filter(s => s) + }, + nba_scoreboard: { + enabled: document.getElementById('nba_enabled').checked, + favorite_teams: document.getElementById('nba_favorites').value.split(',').map(s => s.trim()).filter(s => s) + } + }; + + // Send to server + fetch("{{ url_for('save_config_route') }}", { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + 'config_type': 'main', + 'config_data': JSON.stringify(config) + }) + }) + .then(response => { + if (response.redirected) { + window.location.href = response.url; + } else { + return response.text(); + } + }) + .then(data => { + if (data) { + alert('Sports configuration saved successfully!'); + } + }) + .catch(error => { + alert('Error: ' + error); + }); + } + + // Add form submission handlers for better UX + document.addEventListener('DOMContentLoaded', function() { + // Handle display form submission + const displayForm = document.getElementById('display-form'); + if (displayForm) { + displayForm.addEventListener('submit', function(e) { + e.preventDefault(); + const formData = new FormData(displayForm); + const config = { + display: { + hardware: { + rows: parseInt(formData.get('rows')), + cols: parseInt(formData.get('cols')), + chain_length: parseInt(formData.get('chain_length')), + parallel: parseInt(formData.get('parallel')), + brightness: parseInt(formData.get('brightness')), + hardware_mapping: formData.get('hardware_mapping') + }, + runtime: { + gpio_slowdown: parseInt(formData.get('gpio_slowdown')) + } + } + }; + + fetch("{{ url_for('save_config_route') }}", { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + 'config_type': 'main', + 'config_data': JSON.stringify(config) + }) + }) + .then(response => { + if (response.redirected) { + window.location.href = response.url; + } + }) + .catch(error => { + alert('Error saving display settings: ' + error); + }); + }); + } + + // Handle weather form submission + const weatherForm = document.getElementById('weather-form'); + if (weatherForm) { + weatherForm.addEventListener('submit', function(e) { + e.preventDefault(); + const formData = new FormData(weatherForm); + const config = { + weather: { + enabled: formData.get('weather_enabled') === 'on', + units: formData.get('weather_units'), + update_interval: parseInt(formData.get('weather_update_interval')) + }, + location: { + city: formData.get('weather_city'), + state: formData.get('weather_state') + } + }; + + fetch("{{ url_for('save_config_route') }}", { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + 'config_type': 'main', + 'config_data': JSON.stringify(config) + }) + }) + .then(response => { + if (response.redirected) { + window.location.href = response.url; + } + }) + .catch(error => { + alert('Error saving weather settings: ' + error); + }); + }); + } + + // Handle stocks form submission + const stocksForm = document.getElementById('stocks-form'); + if (stocksForm) { + stocksForm.addEventListener('submit', function(e) { + e.preventDefault(); + const formData = new FormData(stocksForm); + const symbols = formData.get('stocks_symbols').split(',').map(s => s.trim()).filter(s => s); + const config = { + stocks: { + enabled: formData.get('stocks_enabled') === 'on', + symbols: symbols, + update_interval: parseInt(formData.get('stocks_update_interval')) + } + }; + + fetch("{{ url_for('save_config_route') }}", { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + 'config_type': 'main', + 'config_data': JSON.stringify(config) + }) + }) + .then(response => { + if (response.redirected) { + window.location.href = response.url; + } + }) + .catch(error => { + alert('Error saving stocks settings: ' + error); + }); + }); + } + + // Handle crypto form submission + const cryptoForm = document.getElementById('crypto-form'); + if (cryptoForm) { + cryptoForm.addEventListener('submit', function(e) { + e.preventDefault(); + const formData = new FormData(cryptoForm); + const symbols = formData.get('crypto_symbols').split(',').map(s => s.trim()).filter(s => s); + const config = { + crypto: { + enabled: formData.get('crypto_enabled') === 'on', + symbols: symbols, + update_interval: parseInt(formData.get('crypto_update_interval')) + } + }; + + fetch("{{ url_for('save_config_route') }}", { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + 'config_type': 'main', + 'config_data': JSON.stringify(config) + }) + }) + .then(response => { + if (response.redirected) { + window.location.href = response.url; + } + }) + .catch(error => { + alert('Error saving crypto settings: ' + error); + }); + }); + } + + // Handle music form submission + const musicForm = document.getElementById('music-form'); + if (musicForm) { + musicForm.addEventListener('submit', function(e) { + e.preventDefault(); + const formData = new FormData(musicForm); + const config = { + music: { + enabled: formData.get('music_enabled') === 'on', + preferred_source: formData.get('music_preferred_source'), + YTM_COMPANION_URL: formData.get('ytm_companion_url'), + POLLING_INTERVAL_SECONDS: parseInt(formData.get('music_polling_interval')) + } + }; + + fetch("{{ url_for('save_config_route') }}", { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + 'config_type': 'main', + 'config_data': JSON.stringify(config) + }) + }) + .then(response => { + if (response.redirected) { + window.location.href = response.url; + } + }) + .catch(error => { + alert('Error saving music settings: ' + error); + }); + }); + } + + // Handle calendar form submission + const calendarForm = document.getElementById('calendar-form'); + if (calendarForm) { + calendarForm.addEventListener('submit', function(e) { + e.preventDefault(); + const formData = new FormData(calendarForm); + const calendars = formData.get('calendar_calendars').split(',').map(c => c.trim()).filter(c => c); + const config = { + calendar: { + enabled: formData.get('calendar_enabled') === 'on', + max_events: parseInt(formData.get('calendar_max_events')), + update_interval: parseInt(formData.get('calendar_update_interval')), + calendars: calendars + } + }; + + fetch("{{ url_for('save_config_route') }}", { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + 'config_type': 'main', + 'config_data': JSON.stringify(config) + }) + }) + .then(response => { + if (response.redirected) { + window.location.href = response.url; + } + }) + .catch(error => { + alert('Error saving calendar settings: ' + error); + }); + }); + } + + // Handle secrets form submission + const secretsForm = document.getElementById('secrets-form-content'); + if (secretsForm) { + secretsForm.addEventListener('submit', function(e) { + e.preventDefault(); + const formData = new FormData(secretsForm); + const config = { + weather: { + api_key: formData.get('weather_api_key') + }, + youtube: { + api_key: formData.get('youtube_api_key'), + channel_id: formData.get('youtube_channel_id') + }, + music: { + SPOTIFY_CLIENT_ID: formData.get('spotify_client_id'), + SPOTIFY_CLIENT_SECRET: formData.get('spotify_client_secret'), + SPOTIFY_REDIRECT_URI: formData.get('spotify_redirect_uri') + } + }; + + fetch("{{ url_for('save_config_route') }}", { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + 'config_type': 'secrets', + 'config_data': JSON.stringify(config) + }) + }) + .then(response => { + if (response.redirected) { + window.location.href = response.url; + } + }) + .catch(error => { + alert('Error saving API keys: ' + error); + }); + }); + } + }); + function runAction(actionName) { const outputElement = document.getElementById('action_output'); outputElement.textContent = `Running ${actionName.replace(/_/g, ' ')}...`; @@ -180,6 +1103,7 @@ outputElement.textContent = `Error: ${error}`; }); } + // Set default active tab document.addEventListener("DOMContentLoaded", function() { document.querySelector('.tab-link').click(); diff --git a/test_web_interface.py b/test_web_interface.py new file mode 100644 index 00000000..49452d65 --- /dev/null +++ b/test_web_interface.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +""" +Test script for the LED Matrix web interface +This script tests the basic functionality of the web interface +""" + +import requests +import json +import time +import sys + +def test_web_interface(): + """Test the web interface functionality""" + base_url = "http://localhost:5000" + + print("Testing LED Matrix Web Interface...") + print("=" * 50) + + # Test 1: Check if the web interface is running + try: + response = requests.get(base_url, timeout=5) + if response.status_code == 200: + print("✓ Web interface is running") + else: + print(f"✗ Web interface returned status code: {response.status_code}") + return False + except requests.exceptions.ConnectionError: + print("✗ Could not connect to web interface. Is it running?") + print(" Start it with: python3 web_interface.py") + return False + except Exception as e: + print(f"✗ Error connecting to web interface: {e}") + return False + + # Test 2: Test schedule configuration + print("\nTesting schedule configuration...") + schedule_data = { + 'schedule_enabled': 'on', + 'start_time': '08:00', + 'end_time': '22:00' + } + + try: + response = requests.post(f"{base_url}/save_schedule", data=schedule_data, timeout=10) + if response.status_code == 200: + print("✓ Schedule configuration saved successfully") + else: + print(f"✗ Schedule configuration failed: {response.status_code}") + except Exception as e: + print(f"✗ Error saving schedule: {e}") + + # Test 3: Test main configuration save + print("\nTesting main configuration save...") + test_config = { + "weather": { + "enabled": True, + "units": "imperial", + "update_interval": 1800 + }, + "location": { + "city": "Test City", + "state": "Test State" + } + } + + try: + response = requests.post(f"{base_url}/save_config", data={ + 'config_type': 'main', + 'config_data': json.dumps(test_config) + }, timeout=10) + if response.status_code == 200: + print("✓ Main configuration saved successfully") + else: + print(f"✗ Main configuration failed: {response.status_code}") + except Exception as e: + print(f"✗ Error saving main config: {e}") + + # Test 4: Test secrets configuration save + print("\nTesting secrets configuration save...") + test_secrets = { + "weather": { + "api_key": "test_api_key_123" + }, + "youtube": { + "api_key": "test_youtube_key", + "channel_id": "test_channel" + }, + "music": { + "SPOTIFY_CLIENT_ID": "test_spotify_id", + "SPOTIFY_CLIENT_SECRET": "test_spotify_secret", + "SPOTIFY_REDIRECT_URI": "http://127.0.0.1:8888/callback" + } + } + + try: + response = requests.post(f"{base_url}/save_config", data={ + 'config_type': 'secrets', + 'config_data': json.dumps(test_secrets) + }, timeout=10) + if response.status_code == 200: + print("✓ Secrets configuration saved successfully") + else: + print(f"✗ Secrets configuration failed: {response.status_code}") + except Exception as e: + print(f"✗ Error saving secrets: {e}") + + # Test 5: Test action execution + print("\nTesting action execution...") + try: + response = requests.post(f"{base_url}/run_action", + json={'action': 'git_pull'}, + timeout=15) + if response.status_code == 200: + result = response.json() + print(f"✓ Action executed: {result.get('status', 'unknown')}") + if result.get('stderr'): + print(f" Note: {result['stderr']}") + else: + print(f"✗ Action execution failed: {response.status_code}") + except Exception as e: + print(f"✗ Error executing action: {e}") + + print("\n" + "=" * 50) + print("Web interface testing completed!") + print("\nTo start the web interface:") + print("1. Make sure you're on the Raspberry Pi") + print("2. Run: python3 web_interface.py") + print("3. Open a web browser and go to: http://[PI_IP]:5000") + print("\nFeatures available:") + print("- Schedule configuration") + print("- Display hardware settings") + print("- Sports team configuration") + print("- Weather settings") + print("- Stocks & crypto configuration") + print("- Music settings") + print("- Calendar configuration") + print("- API key management") + print("- System actions (start/stop display, etc.)") + + return True + +if __name__ == "__main__": + success = test_web_interface() + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/web_interface.py b/web_interface.py index d4156fc1..cb72f70b 100644 --- a/web_interface.py +++ b/web_interface.py @@ -24,13 +24,17 @@ def index(): schedule_config = {} main_config_json = "{}" secrets_config_json = "{}" + main_config_data = {} + secrets_config_data = {} 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()) + secrets_config_path=config_manager.get_secrets_path(), + main_config=main_config_data, + secrets_config=secrets_config_data) @app.route('/save_schedule', methods=['POST']) def save_schedule_route(): @@ -59,9 +63,109 @@ def save_config_route(): 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") + if config_type == 'main': + # Handle form-based configuration updates + main_config = config_manager.load_config() + + # Update display settings + if 'rows' in request.form: + main_config['display']['hardware']['rows'] = int(request.form.get('rows', 32)) + main_config['display']['hardware']['cols'] = int(request.form.get('cols', 64)) + main_config['display']['hardware']['chain_length'] = int(request.form.get('chain_length', 2)) + main_config['display']['hardware']['parallel'] = int(request.form.get('parallel', 1)) + main_config['display']['hardware']['brightness'] = int(request.form.get('brightness', 95)) + main_config['display']['hardware']['hardware_mapping'] = request.form.get('hardware_mapping', 'adafruit-hat-pwm') + main_config['display']['runtime']['gpio_slowdown'] = int(request.form.get('gpio_slowdown', 3)) + + # Update weather settings + if 'weather_enabled' in request.form: + main_config['weather']['enabled'] = 'weather_enabled' in request.form + main_config['location']['city'] = request.form.get('weather_city', 'Dallas') + main_config['location']['state'] = request.form.get('weather_state', 'Texas') + main_config['weather']['units'] = request.form.get('weather_units', 'imperial') + main_config['weather']['update_interval'] = int(request.form.get('weather_update_interval', 1800)) + + # Update stocks settings + if 'stocks_enabled' in request.form: + main_config['stocks']['enabled'] = 'stocks_enabled' in request.form + symbols = request.form.get('stocks_symbols', '').split(',') + main_config['stocks']['symbols'] = [s.strip() for s in symbols if s.strip()] + main_config['stocks']['update_interval'] = int(request.form.get('stocks_update_interval', 600)) + + # Update crypto settings + if 'crypto_enabled' in request.form: + main_config['crypto']['enabled'] = 'crypto_enabled' in request.form + symbols = request.form.get('crypto_symbols', '').split(',') + main_config['crypto']['symbols'] = [s.strip() for s in symbols if s.strip()] + main_config['crypto']['update_interval'] = int(request.form.get('crypto_update_interval', 600)) + + # Update music settings + if 'music_enabled' in request.form: + main_config['music']['enabled'] = 'music_enabled' in request.form + main_config['music']['preferred_source'] = request.form.get('music_preferred_source', 'ytm') + main_config['music']['YTM_COMPANION_URL'] = request.form.get('ytm_companion_url', 'http://192.168.86.12:9863') + main_config['music']['POLLING_INTERVAL_SECONDS'] = int(request.form.get('music_polling_interval', 1)) + + # Update calendar settings + if 'calendar_enabled' in request.form: + main_config['calendar']['enabled'] = 'calendar_enabled' in request.form + main_config['calendar']['max_events'] = int(request.form.get('calendar_max_events', 3)) + main_config['calendar']['update_interval'] = int(request.form.get('calendar_update_interval', 3600)) + calendars = request.form.get('calendar_calendars', '').split(',') + main_config['calendar']['calendars'] = [c.strip() for c in calendars if c.strip()] + + # If config_data is provided as JSON, merge it + if config_data_str: + try: + new_data = json.loads(config_data_str) + # Merge the new data with existing config + for key, value in new_data.items(): + if key in main_config: + if isinstance(value, dict) and isinstance(main_config[key], dict): + main_config[key].update(value) + else: + main_config[key] = value + else: + main_config[key] = value + except json.JSONDecodeError: + flash("Error: Invalid JSON format in config data.", "error") + return redirect(url_for('index')) + + config_manager.save_config(main_config) + flash("Main configuration saved successfully!", "success") + + elif config_type == 'secrets': + # Handle secrets configuration + secrets_config = config_manager.get_raw_file_content('secrets') + + # Update weather API key + if 'weather_api_key' in request.form: + secrets_config['weather']['api_key'] = request.form.get('weather_api_key', '') + + # Update YouTube API settings + if 'youtube_api_key' in request.form: + secrets_config['youtube']['api_key'] = request.form.get('youtube_api_key', '') + secrets_config['youtube']['channel_id'] = request.form.get('youtube_channel_id', '') + + # Update Spotify API settings + if 'spotify_client_id' in request.form: + secrets_config['music']['SPOTIFY_CLIENT_ID'] = request.form.get('spotify_client_id', '') + secrets_config['music']['SPOTIFY_CLIENT_SECRET'] = request.form.get('spotify_client_secret', '') + secrets_config['music']['SPOTIFY_REDIRECT_URI'] = request.form.get('spotify_redirect_uri', 'http://127.0.0.1:8888/callback') + + # If config_data is provided as JSON, use it + if config_data_str: + try: + new_data = json.loads(config_data_str) + config_manager.save_raw_file_content('secrets', new_data) + except json.JSONDecodeError: + flash("Error: Invalid JSON format for secrets config.", "error") + return redirect(url_for('index')) + else: + config_manager.save_raw_file_content('secrets', secrets_config) + + flash("Secrets configuration saved successfully!", "success") + except json.JSONDecodeError: flash(f"Error: Invalid JSON format for {config_type} config.", "error") except Exception as e: