"""Live service monitor — like `avahi-browse` without -t. Prints events as they happen: + service appeared = service appeared and was resolved (address:port) - service disappeared With no --type it browses the DNS-SD meta-service (`_services._dns-sd._udp.local.`) to discover types dynamically and attaches a browser to each — the equivalent of `avahi-browse -a`. """ import time from zeroconf import Zeroconf, ServiceBrowser, ServiceListener class _EventListener(ServiceListener): def __init__(self, resolve: bool = True) -> None: self.resolve = resolve def add_service(self, zc: Zeroconf, type_: str, name: str) -> None: # noqa: N802 line = f"+ {type_:<30} {name}" if self.resolve: info = zc.get_service_info(type_, name, timeout=1500) if info: addrs = ", ".join(info.parsed_addresses() or []) or "-" line = f"= {type_:<30} {name} {addrs}:{info.port}" print(line, flush=True) def update_service(self, zc: Zeroconf, type_: str, name: str) -> None: # noqa: N802 return def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None: # noqa: N802 print(f"- {type_:<30} {name}", flush=True) class _TypeSpawner(ServiceListener): """On discovering a new service TYPE, attach a browser for it.""" def __init__(self, zc: Zeroconf, svc_listener: ServiceListener) -> None: self.zc = zc self.svc_listener = svc_listener self.browsers: dict[str, ServiceBrowser] = {} def add_service(self, zc: Zeroconf, type_: str, name: str) -> None: # noqa: N802 # For the meta-service, `name` is the discovered type (e.g. _http._tcp.local.) if name not in self.browsers: self.browsers[name] = ServiceBrowser(zc, name, self.svc_listener) def update_service(self, *a) -> None: # noqa: N802 return def remove_service(self, *a) -> None: # noqa: N802 return def watch(service_type: str | None = None, resolve: bool = True) -> None: zc = Zeroconf() svc = _EventListener(resolve=resolve) try: if service_type: ServiceBrowser(zc, service_type, svc) else: ServiceBrowser(zc, "_services._dns-sd._udp.local.", _TypeSpawner(zc, svc)) print("[mdns] watching — Ctrl+C to stop", flush=True) while True: time.sleep(1) except KeyboardInterrupt: pass finally: zc.close()