mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 10:51:36 -04:00
169 lines
5 KiB
Python
169 lines
5 KiB
Python
"""
|
|
Configuration management for the Controller.
|
|
|
|
Controller uses code-defined defaults and optionally loads overrides
|
|
from controller.yaml on the WROLPi drive after it's mounted.
|
|
"""
|
|
|
|
import copy
|
|
import logging
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import yaml
|
|
|
|
from controller.defaults import DEFAULT_CONFIG
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Path to config file on the WROLPi drive
|
|
CONFIG_PATH_ON_DRIVE = Path("/media/wrolpi/config/controller.yaml")
|
|
|
|
# Runtime config - starts as defaults, updated after drive mount
|
|
_runtime_config: dict = copy.deepcopy(DEFAULT_CONFIG)
|
|
|
|
|
|
def get_config() -> dict:
|
|
"""Get the current runtime configuration."""
|
|
return _runtime_config
|
|
|
|
|
|
def get_config_value(key: str, default: Any = None) -> Any:
|
|
"""Get a specific config value by dot-notation key (e.g., 'drives.auto_mount')."""
|
|
keys = key.split(".")
|
|
value = _runtime_config
|
|
for k in keys:
|
|
if isinstance(value, dict) and k in value:
|
|
value = value[k]
|
|
else:
|
|
return default
|
|
return value
|
|
|
|
|
|
def is_docker_mode() -> bool:
|
|
"""Check if running in Docker container."""
|
|
return os.environ.get("DOCKERIZED", "").lower() == "true"
|
|
|
|
|
|
# Raspberry Pi detection
|
|
RPI_DEVICE_PATH = Path('/proc/device-tree/model')
|
|
_RPI_DEVICE_MODEL_CONTENTS = RPI_DEVICE_PATH.read_text() if RPI_DEVICE_PATH.is_file() else None
|
|
|
|
|
|
def is_rpi4() -> bool:
|
|
"""Check if running on Raspberry Pi 4."""
|
|
return 'Raspberry Pi 4' in _RPI_DEVICE_MODEL_CONTENTS if _RPI_DEVICE_MODEL_CONTENTS else False
|
|
|
|
|
|
def is_rpi5() -> bool:
|
|
"""Check if running on Raspberry Pi 5."""
|
|
return 'Raspberry Pi 5' in _RPI_DEVICE_MODEL_CONTENTS if _RPI_DEVICE_MODEL_CONTENTS else False
|
|
|
|
|
|
def is_rpi() -> bool:
|
|
"""Check if running on any Raspberry Pi."""
|
|
return is_rpi4() or is_rpi5()
|
|
|
|
|
|
def get_media_directory() -> Path:
|
|
"""Get the media directory path."""
|
|
return Path(os.environ.get("MEDIA_DIRECTORY", "/media/wrolpi"))
|
|
|
|
|
|
def is_primary_drive_mounted() -> bool:
|
|
"""Check if the primary WROLPi drive is mounted."""
|
|
return Path("/media/wrolpi/config").exists()
|
|
|
|
|
|
def reload_config_from_drive() -> bool:
|
|
"""
|
|
Load config from the mounted WROLPi drive and merge with defaults.
|
|
Called after the primary drive is mounted.
|
|
|
|
Returns True if config was loaded, False if no config file found.
|
|
"""
|
|
global _runtime_config
|
|
|
|
if not CONFIG_PATH_ON_DRIVE.exists():
|
|
# No config file on drive - keep defaults
|
|
return False
|
|
|
|
try:
|
|
with open(CONFIG_PATH_ON_DRIVE) as f:
|
|
drive_config = yaml.safe_load(f) or {}
|
|
except (IOError, yaml.YAMLError) as e:
|
|
# Failed to load - keep defaults
|
|
logger.warning("Failed to load controller.yaml: %s", e)
|
|
return False
|
|
|
|
# Deep merge: drive_config overrides defaults
|
|
_runtime_config = _deep_merge(copy.deepcopy(DEFAULT_CONFIG), drive_config)
|
|
return True
|
|
|
|
|
|
def save_config() -> None:
|
|
"""
|
|
Save current runtime config to the WROLPi drive.
|
|
Only saves the diff from defaults to keep the file clean.
|
|
If config matches defaults, removes the config file.
|
|
"""
|
|
if not is_primary_drive_mounted():
|
|
raise RuntimeError("Cannot save config: primary drive not mounted")
|
|
|
|
# Calculate diff from defaults
|
|
diff = _get_config_diff(_runtime_config, DEFAULT_CONFIG)
|
|
|
|
if not diff:
|
|
# Config matches defaults - remove config file if it exists
|
|
if CONFIG_PATH_ON_DRIVE.exists():
|
|
CONFIG_PATH_ON_DRIVE.unlink()
|
|
return
|
|
|
|
CONFIG_PATH_ON_DRIVE.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(CONFIG_PATH_ON_DRIVE, "w") as f:
|
|
yaml.dump(diff, f, default_flow_style=False, sort_keys=False)
|
|
|
|
|
|
def update_config(key: str, value: Any) -> None:
|
|
"""
|
|
Update a config value by dot-notation key.
|
|
Does not automatically save - call save_config() after updates.
|
|
"""
|
|
keys = key.split(".")
|
|
config = _runtime_config
|
|
|
|
# Navigate to parent of target key
|
|
for k in keys[:-1]:
|
|
if k not in config:
|
|
config[k] = {}
|
|
config = config[k]
|
|
|
|
# Set the value
|
|
config[keys[-1]] = value
|
|
|
|
|
|
def _deep_merge(base: dict, override: dict) -> dict:
|
|
"""Deep merge two dicts. Override values take precedence."""
|
|
result = base.copy()
|
|
for key, value in override.items():
|
|
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
result[key] = _deep_merge(result[key], value)
|
|
else:
|
|
result[key] = value
|
|
return result
|
|
|
|
|
|
def _get_config_diff(current: dict, default: dict) -> dict:
|
|
"""Get the difference between current config and defaults."""
|
|
diff = {}
|
|
for key, value in current.items():
|
|
if key not in default:
|
|
diff[key] = value
|
|
elif isinstance(value, dict) and isinstance(default.get(key), dict):
|
|
nested_diff = _get_config_diff(value, default[key])
|
|
if nested_diff:
|
|
diff[key] = nested_diff
|
|
elif value != default.get(key):
|
|
diff[key] = value
|
|
return diff
|