Files
led-controller/src/settings.py
pi 827eb97203 feat(settings): server global brightness and Wi-Fi driver resync
- Serve GET /settings as JSON by removing duplicate HTML route (use /settings/page for the standalone UI).

- Save global_brightness via PUT; broadcast to connected drivers; push saved level when outbound WS connects.

- Zones UI loads brightness from GET /settings only (no localStorage).

- Bump led-driver submodule for settings.save on brightness with save flag.

- Extend API doc and endpoint tests for global_brightness.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-03 22:15:30 +12:00

106 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
import os
import binascii
def _settings_path():
"""Path to settings.json in project root (writable without root)."""
try:
base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
return os.path.join(base, "settings.json")
except Exception:
return "settings.json"
class Settings(dict):
SETTINGS_FILE = None # Set in __init__ from _settings_path()
def __init__(self):
super().__init__()
if Settings.SETTINGS_FILE is None:
Settings.SETTINGS_FILE = _settings_path()
self.load() # Load settings from file during initialization
def generate_secret_key(self):
"""Generate a random secret key for session signing."""
try:
# Try to use os.urandom for secure random bytes
random_bytes = os.urandom(32)
return binascii.hexlify(random_bytes).decode('utf-8')
except (AttributeError, NotImplementedError):
# Fallback for MicroPython or systems without os.urandom
try:
import secrets
return secrets.token_hex(32)
except ImportError:
# Last resort: use a combination of time and random
import time
import random
random.seed(time.time())
return binascii.hexlify(bytes([random.randint(0, 255) for _ in range(32)])).decode('utf-8')
def set_defaults(self):
"""Set default settings if they don't exist."""
if 'session_secret_key' not in self:
self['session_secret_key'] = self.generate_secret_key()
# Save immediately when generating a new key
self.save()
# ESP-NOW STA channel (2.4 GHz) for LED drivers / bridge alignment; 111
if 'wifi_channel' not in self:
self['wifi_channel'] = 6
# Wi-Fi LED drivers: controller opens WebSocket to device (firmware serves /ws)
if 'wifi_driver_ws_port' not in self:
self['wifi_driver_ws_port'] = 80
if 'wifi_driver_ws_path' not in self:
self['wifi_driver_ws_path'] = '/ws'
# Seconds between UDP discovery nudges when a Wi-Fi driver WebSocket is
# down (0 disables). Helps drivers that reconnect after seeing traffic on 8766.
if 'wifi_driver_hello_interval_s' not in self:
self['wifi_driver_hello_interval_s'] = 10.0
# Outbound WebSocket dial: total seconds to keep trying before first success
# (many devices booting at once need more than a short window).
if 'wifi_driver_connect_retry_window_s' not in self:
self['wifi_driver_connect_retry_window_s'] = 120.0
# Spread outbound dials 0..N s by device IP so six+ drivers do not all hit the AP at once.
if 'wifi_driver_connect_stagger_max_s' not in self:
self['wifi_driver_connect_stagger_max_s'] = 2.5
# TCP/WebSocket open timeout per attempt (seconds).
if 'wifi_driver_ws_open_timeout' not in self:
self['wifi_driver_ws_open_timeout'] = 45.0
# Pause between outbound WebSocket dial attempts (seconds).
if 'wifi_driver_connect_retry_interval_s' not in self:
self['wifi_driver_connect_retry_interval_s'] = 2.0
# UART to ESP32 ESP-NOW bridge; default off (Wi-Fi drivers need no serial).
if 'serial_enabled' not in self:
self['serial_enabled'] = False
# Zone UI global brightness (0255); shared across browsers/devices.
if 'global_brightness' not in self:
self['global_brightness'] = 255
def save(self):
try:
j = json.dumps(self)
with open(self.SETTINGS_FILE, 'w') as file:
file.write(j)
print("Settings saved successfully.")
except Exception as e:
print(f"Error saving settings: {e}")
def load(self):
loaded_from_file = False
try:
with open(self.SETTINGS_FILE, 'r') as file:
loaded_settings = json.load(file)
self.update(loaded_settings)
loaded_from_file = True
print("Settings loaded successfully.")
except Exception as e:
print(f"Error loading settings")
self.clear()
finally:
# Ensure defaults are set even if file exists but is missing keys
self.set_defaults()
# Only save if file didn't exist or was invalid
if not loaded_from_file:
self.save()