"""Service discovery on the local network (DNS-SD). browse(service_type=None, duration=3.0) -> list of dicts """ import time from zeroconf import Zeroconf, ServiceBrowser, ServiceListener, ZeroconfServiceTypes class _Collector(ServiceListener): def __init__(self) -> None: self.items: dict[str, dict] = {} def add_service(self, zc: Zeroconf, type_: str, name: str) -> None: # noqa: N802 info = zc.get_service_info(type_, name, timeout=1500) if info is None: return addrs = [a for a in (info.parsed_addresses() or [])] self.items[name] = { "name": name, "type": type_, "server": (info.server or "").rstrip("."), "port": info.port, "addresses": addrs, "txt": { k.decode(errors="replace"): (v.decode(errors="replace") if isinstance(v, (bytes, bytearray)) else v) for k, v in (info.properties or {}).items() if k }, } def update_service(self, *a, **k) -> None: # noqa: N802 return def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None: # noqa: N802 self.items.pop(name, None) def list_types(duration: float = 3.0) -> list[str]: """Return all service types advertised on the LAN.""" return sorted(ZeroconfServiceTypes.find(timeout=duration)) def browse(service_type: str | None = None, duration: float = 3.0) -> list[dict]: """Discover services. If service_type is None, discover all types first.""" zc = Zeroconf() try: types = [service_type] if service_type else list_types(duration=duration / 2) if not types: return [] listener = _Collector() browsers = [ServiceBrowser(zc, t, listener) for t in types] time.sleep(duration if not service_type else duration) for b in browsers: b.cancel() return sorted(listener.items.values(), key=lambda x: (x.get("type", ""), x.get("name", ""))) finally: zc.close()