webui rework

This commit is contained in:
Chuck
2025-07-24 13:31:24 -05:00
parent 8c705753df
commit 9d01996ae6
4 changed files with 1468 additions and 78 deletions

218
WEB_INTERFACE_README.md Normal file
View File

@@ -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.

File diff suppressed because it is too large Load Diff

144
test_web_interface.py Normal file
View File

@@ -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)

View File

@@ -24,13 +24,17 @@ def index():
schedule_config = {} schedule_config = {}
main_config_json = "{}" main_config_json = "{}"
secrets_config_json = "{}" secrets_config_json = "{}"
main_config_data = {}
secrets_config_data = {}
return render_template('index.html', return render_template('index.html',
schedule_config=schedule_config, schedule_config=schedule_config,
main_config_json=main_config_json, main_config_json=main_config_json,
secrets_config_json=secrets_config_json, secrets_config_json=secrets_config_json,
main_config_path=config_manager.get_config_path(), 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']) @app.route('/save_schedule', methods=['POST'])
def save_schedule_route(): def save_schedule_route():
@@ -58,10 +62,110 @@ def save_config_route():
config_type = request.form.get('config_type') config_type = request.form.get('config_type')
config_data_str = request.form.get('config_data') config_data_str = request.form.get('config_data')
try:
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: try:
new_data = json.loads(config_data_str) new_data = json.loads(config_data_str)
config_manager.save_raw_file_content(config_type, new_data) # Merge the new data with existing config
flash(f"{config_type.capitalize()} configuration saved successfully!", "success") 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: except json.JSONDecodeError:
flash(f"Error: Invalid JSON format for {config_type} config.", "error") flash(f"Error: Invalid JSON format for {config_type} config.", "error")
except Exception as e: except Exception as e: