Files
Daniel Duran dfacfeade8 feat: bundled MC2MQTT broker presets (waev, letsmesh) + format family
Introduces a 'set format and forget' workflow for MQTT brokers. Users
reference a bundled preset by name inside the existing brokers: list,
and the package supplies the endpoints, audiences, and TLS settings.
Endpoint changes ship via 'pip install -U' instead of manual edits.

What changes
- New repeater/presets/ package with a tiny lazy YAML loader and two
  bundled presets: waev (mqtt-{a,b}.waev.app) and letsmesh (EU + US).
- New format-family constant MC2MQTT_FORMATS = ('meshcoretomqtt',
  'letsmesh', 'waev') replaces the inline tuple in topic resolution.
  The legacy 'mqtt' format keeps its custom-topic semantics unchanged.
- Two-pass broker assembly in mqtt_handler.py: pass 1 expands every
  {preset: <name>} entry inline; pass 2 collapses duplicates by name
  with later-wins semantics. Place override entries AFTER preset
  entries.
- Hard-coded LETSMESH_BROKERS constant deleted; its data now lives in
  repeater/presets/letsmesh.yaml.
- convert_letsmesh_to_broker_config() collapsed from ~70 to ~25 lines
  by emitting {preset: letsmesh} plus disable overrides for unwanted
  brokers. Honors broker_index in (-1, 0, 1), additional_brokers, and
  enabled flag exactly as before.
- update_mqtt_config API endpoint accepts {preset: <name>} entries and
  passes them through unchanged so the web UI can author them when the
  frontend is updated.
- config.yaml.example documents the preset entry shape, the override
  rule, and the format family hierarchy.
- pyproject.toml ships presets/*.yaml as package data.

How to use
  mqtt_brokers:
    iata_code: "LAX"
    brokers:
      - preset: waev

  # Override a single preset broker:
  brokers:
    - preset: waev
    - name: waev-b
      enabled: false

Tests
- tests/test_presets.py: 9 tests covering loader, expand/merge,
  MC2MQTT topic-family parity, and parametrized legacy migration.

Co-Authored-By: Oz <oz-agent@warp.dev>
2026-05-02 15:32:22 -07:00

61 lines
1.9 KiB
Python

"""Bundled MQTT broker presets.
Each sibling ``*.yaml`` file in this package defines a named preset - a
ready-to-use list of broker dicts for a known MeshCoreToMQTT (MC2MQTT)
network. Presets ship with the package, so a ``pip install -U`` is enough
to pick up new endpoints without editing user config.
Public API:
get_preset(name) -> dict (the parsed YAML, or {} if unknown)
list_presets() -> sorted list of preset names
The loader is lazy: nothing is read or parsed at import time. The first
call discovers sibling YAML files via ``importlib.resources`` and caches
the parsed dicts for the lifetime of the process.
"""
from __future__ import annotations
import logging
from importlib.resources import files
from pathlib import Path
from typing import Dict, List
import yaml
logger = logging.getLogger("Presets")
# Cache of parsed presets, keyed by name (e.g. "waev"). Populated on first
# call to _load_all(); never cleared.
_CACHE: Dict[str, dict] = {}
_LOADED: bool = False
def _load_all() -> Dict[str, dict]:
"""Discover and parse every bundled ``*.yaml`` file once."""
global _LOADED
if _LOADED:
return _CACHE
for resource in files(__package__).iterdir():
# importlib.resources.Traversable: only consider real files ending in .yaml
name = getattr(resource, "name", "")
if not name.endswith(".yaml"):
continue
try:
with resource.open("r", encoding="utf-8") as f:
_CACHE[Path(name).stem] = yaml.safe_load(f) or {}
except Exception as e: # pragma: no cover - defensive
logger.warning(f"Failed to load preset '{name}': {e}")
_LOADED = True
return _CACHE
def get_preset(name: str) -> dict:
"""Return the parsed preset dict, or ``{}`` if no such preset exists."""
return _load_all().get(name, {})
def list_presets() -> List[str]:
"""Return the sorted list of bundled preset names."""
return sorted(_load_all().keys())