Files
led-controller/src/settings.py
2026-05-28 00:38:21 +12:00

130 lines
4.9 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
WIFI_CHANNEL_DEFAULT = 5
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"
_settings_singleton: "Settings | None" = None
class Settings(dict):
SETTINGS_FILE = None # Set in __init__ from _settings_path()
def __init__(self, *, quiet: bool = False):
super().__init__()
self._quiet = quiet
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'] = WIFI_CHANNEL_DEFAULT
# WebSocket URL of ESP-NOW bridge (Pi is client), e.g. ws://192.168.4.1/ws
if 'bridge_ws_url' not in self:
self['bridge_ws_url'] = ''
if 'wifi_interface' not in self:
self['wifi_interface'] = ''
if 'bridges' not in self:
self['bridges'] = []
if 'bridge_transport' not in self:
self['bridge_transport'] = 'serial'
if 'bridge_serial_port' not in self:
self['bridge_serial_port'] = ''
if 'bridge_serial_baudrate' not in self:
self['bridge_serial_baudrate'] = 115200
# Zone UI global brightness (0255); shared across browsers/devices.
if 'global_brightness' not in self:
self['global_brightness'] = 255
# Sequence tile start: wait for beat or downbeat (server-owned).
if 'sequence_switch_wait' not in self:
self['sequence_switch_wait'] = 'beat'
elif str(self.get('sequence_switch_wait', '')).strip().lower() == 'phrase':
self['sequence_switch_wait'] = 'beat'
# Beat flash alignment delay (ms); applied by all UI clients polling audio status.
if 'audio_beat_phase_ms' not in self:
self['audio_beat_phase_ms'] = 0
# Input gain for beat detection (percent, 0200).
if 'audio_input_volume' not in self:
self['audio_input_volume'] = 100
def save(self):
try:
j = json.dumps(self, indent=2, sort_keys=True)
with open(self.SETTINGS_FILE, 'w') as file:
file.write(j)
file.write("\n")
if not getattr(self, "_quiet", False):
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
if not getattr(self, "_quiet", False):
print("Settings loaded successfully.")
except Exception as e:
if not getattr(self, "_quiet", False):
print(f"Error loading settings: {e}")
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()
def get_settings() -> Settings:
"""Process-wide settings instance (avoid re-reading settings.json on every request)."""
global _settings_singleton
if _settings_singleton is None:
_settings_singleton = Settings()
return _settings_singleton
def reload_settings() -> Settings:
"""Re-read settings.json (e.g. after external file edit)."""
global _settings_singleton
_settings_singleton = Settings(quiet=True)
return _settings_singleton