From cc9df8fb7394012fbd65027b70de705540ff0e7d Mon Sep 17 00:00:00 2001 From: Donald Clark Jackson Date: Sun, 28 Jun 2026 08:28:28 -0700 Subject: [PATCH] examples/utility-meter: add --discover (mDNS broker discovery) The utility-meter can now find its broker over mDNS instead of needing an explicit host in the broker config. With --discover it browses _secure-mqtt._tcp, resolves the advertised host/port (preferring the spec `broker` TXT, falling back to the SRV target), and overrides host/port in the broker config; the broker config still supplies the TLS material. Falls back to the config host on timeout. Uses the existing optional `mdns` extra (zeroconf). Documented in examples/README. Co-Authored-By: Claude Opus 4.8 (1M context) --- examples/README.md | 6 +++ examples/utility-meter | 86 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/examples/README.md b/examples/README.md index dae9ec3..450f50e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -28,6 +28,12 @@ Publishes a single eBus utility-meter device (`energy.ebus.device.utility-meter` ./utility-meter --config ./utility-meter-cfg.example.json --broker-config /path/to/broker-cfg.json ``` +Add `--discover` to find the broker over mDNS (`_secure-mqtt._tcp`) instead of using the `host`/`port` in the broker config; the broker config still supplies the TLS material. Needs the `mdns` extra (`pip install 'ebus-sdk[mdns]'`): + +```bash +./utility-meter --config ./utility-meter-cfg.example.json --broker-config /path/to/broker-cfg.json --discover +``` + Set DOE values at runtime: ```bash diff --git a/examples/utility-meter b/examples/utility-meter index 4124264..fb1c705 100755 --- a/examples/utility-meter +++ b/examples/utility-meter @@ -550,6 +550,56 @@ def start_tick_thread(meter: UtilityMeter, interval_s: float) -> threading.Event # ─── Entry point ────────────────────────────────────────────────────────────── +def _discover_broker( + timeout: float, log: logging.Logger +) -> Optional[tuple]: + """Discover an eBus broker over mDNS (`_secure-mqtt._tcp`). + + Returns (host, port) from the advertisement, preferring the spec `broker` + TXT record (the `.local` name the broker's cert SAN covers) and + falling back to the SRV target. Returns None on timeout. Requires the + optional `mdns` extra (zeroconf). + """ + try: + from zeroconf import ServiceBrowser, ServiceListener, Zeroconf + except ImportError: + log.error( + "discovery needs the 'mdns' extra: pip install 'ebus-sdk[mdns]'" + ) + return None + + service_type = "_secure-mqtt._tcp.local." + found = threading.Event() + result: Dict[str, Any] = {} + + class _Listener(ServiceListener): + def add_service(self, zc, type_, name): + info = zc.get_service_info(type_, name, timeout=3000) + if info is None: + return + broker = info.properties.get(b"broker") + result["host"] = ( + broker.decode() if broker else info.server.rstrip(".") + ) + result["port"] = info.port or 8883 + found.set() + + def update_service(self, zc, type_, name): + self.add_service(zc, type_, name) + + def remove_service(self, zc, type_, name): + pass + + zc = Zeroconf() + ServiceBrowser(zc, service_type, _Listener()) + try: + if found.wait(timeout): + return result["host"], result["port"] + return None + finally: + zc.close() + + def main(): parser = argparse.ArgumentParser( description=( @@ -569,6 +619,19 @@ def main(): help="Path to the MQTT broker config JSON file. " f"Defaults to ${DEFAULT_BROKER_CFG_ENV} env var.", ) + parser.add_argument( + "--discover", + action="store_true", + help="Discover the broker over mDNS (_secure-mqtt._tcp) and use the " + "advertised host/port, overriding 'host'/'port' in the broker config. " + "The broker config still supplies the TLS material.", + ) + parser.add_argument( + "--discover-timeout", + type=float, + default=8.0, + help="Seconds to wait for mDNS broker discovery (default: 8.0).", + ) parser.add_argument( "--doe-port", type=int, @@ -604,6 +667,29 @@ def main(): sys.exit(2) mqtt_cfg = UtilityMeterAdapter.load_broker_config(broker_cfg_path) + # Prefer an mDNS-discovered broker over the config's explicit host, per the + # eBus broker-discovery flow. The broker config still supplies the TLS + # material; discovery only fills in where to connect. + if args.discover: + endpoint = _discover_broker(args.discover_timeout, log) + if endpoint is not None: + mqtt_cfg["host"], mqtt_cfg["port"] = endpoint + log.info( + "reason=brokerDiscovered,host=%s,port=%d", + mqtt_cfg["host"], + mqtt_cfg["port"], + ) + elif mqtt_cfg.get("host"): + log.warning( + "reason=brokerDiscoveryTimeout,fallbackHost=%s", + mqtt_cfg["host"], + ) + else: + log.error( + "broker discovery failed and no fallback 'host' in broker config" + ) + sys.exit(2) + # Resolve device identity from config device_id = meter_cfg.get("device-id") or meter_cfg.get("info", {}).get( "serial-number"