mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 13:02:59 +00:00
webui rework
This commit is contained in:
218
WEB_INTERFACE_README.md
Normal file
218
WEB_INTERFACE_README.md
Normal 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.
|
||||
1072
templates/index.html
1072
templates/index.html
File diff suppressed because it is too large
Load Diff
144
test_web_interface.py
Normal file
144
test_web_interface.py
Normal 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)
|
||||
112
web_interface.py
112
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:
|
||||
|
||||
Reference in New Issue
Block a user