wrolpi/modules/flasher/config.py
lrnselfreliance c3e25b84d6
Configs without database rows read and validate only; imports run off the event loop (#874)
The base import for a config with no sections now reads its file once, reports a 0-byte file as empty, treats a
missing file as the defaults, and records success or failure itself (sending the failure Event on request).  The
per-config overrides that only set successful_import are removed.

wrolpi.yaml and the downloader settings import in a thread at startup (import_file_configs), and the Import, Save
and backup Import buttons run their config work in a thread.

The pre-diff import path (import_decision, record_import_version) has no callers left and is removed.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:17:31 -06:00

81 lines
2.9 KiB
Python

"""A YAML config storing the user's saved firmware flashing configurations.
Each saved configuration is a named set of firmware files (media-relative paths) and their flash offsets, plus
whether to erase the flash first. This lets a user configure a multi-part flash once (e.g. a Meshtastic T-Deck:
firmware at 0x0, littlefs at 0xc90000) and re-load it later with one click.
"""
from dataclasses import dataclass, field
from typing import List
from wrolpi.common import ConfigFile, logger
logger = logger.getChild(__name__)
@dataclass
class FlasherConfigValidator:
version: int = None
configurations: list = field(default_factory=list)
class FlasherConfig(ConfigFile):
file_name = 'flasher.yaml'
default_config = dict(
version=0,
configurations=[],
)
validator = FlasherConfigValidator
@property
def configurations(self) -> List[dict]:
return list(self._config.get('configurations', []))
@configurations.setter
def configurations(self, value: List[dict]):
# ConfigFile.update() validates, stores, and triggers a background save.
self.update({'configurations': value})
def save_configuration(self, name: str, files: List[dict], erase_all: bool = False) -> dict:
"""Add or replace (by name) a saved configuration. Returns the stored configuration."""
name = (name or '').strip()
if not name:
raise ValueError('Configuration name is required')
# Keep only the fields we persist for each file.
stored_files = [
dict(
path=f.get('path'),
address=f.get('address'),
name=f.get('name'),
size=f.get('size'),
)
for f in files
]
configuration = dict(name=name, erase_all=bool(erase_all), files=stored_files)
# Replace any existing configuration with the same name, then keep the list sorted by name.
configurations = [c for c in self.configurations if c.get('name') != name]
configurations.append(configuration)
configurations.sort(key=lambda c: (c.get('name') or '').lower())
self.configurations = configurations
return configuration
def delete_configuration(self, name: str) -> bool:
"""Delete a saved configuration by name. Returns True if one was removed."""
configurations = self.configurations
remaining = [c for c in configurations if c.get('name') != name]
if len(remaining) == len(configurations):
return False
self.configurations = remaining
return True
FLASHER_CONFIG: FlasherConfig = FlasherConfig()
# Test override (see get_flasher_config).
TEST_FLASHER_CONFIG = None
def get_flasher_config() -> FlasherConfig:
global TEST_FLASHER_CONFIG
if isinstance(TEST_FLASHER_CONFIG, ConfigFile):
return TEST_FLASHER_CONFIG
return FLASHER_CONFIG