fix(mdns): accumulate probe records across responses; unit tests [#619]
Live proof: ecobee answers unicast mDNS with PTR only (_hap._tcp.local -> "Main Floor._hap._tcp.local"), so the old replace-on-probe wiped learned records every cycle. Merge by (name, type) instead. CONFIG_PATH now env-overridable for tests. Details: https://projects.knownelement.com/issues/619#note-5 💘 Generated with Crush Assisted-by: Crush:glm-5.2
This commit is contained in:
@@ -19,13 +19,14 @@ Design notes:
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
import socket
|
||||
import struct
|
||||
import threading
|
||||
import time
|
||||
|
||||
CONFIG_PATH = "/usr/local/etc/hap-bridge.conf"
|
||||
CONFIG_PATH = os.environ.get("CONFIG_PATH", "/usr/local/etc/hap-bridge.conf")
|
||||
MDNS_GROUP = "224.0.0.251"
|
||||
MDNS_PORT = 5353
|
||||
REFRESH_INTERVAL = 60
|
||||
@@ -60,6 +61,24 @@ def encode_name(name):
|
||||
return out + b"\x00"
|
||||
|
||||
|
||||
def decode_rr_header(data, off):
|
||||
"""Return (rtype, rclass, ttl, rdata_len) for the RR header at offset."""
|
||||
return struct.unpack(">HHIH", data[off:off + 10])
|
||||
|
||||
|
||||
def merge_records(prev_recs, new_recs):
|
||||
"""Union record sets by (name, type); new records replace same-key old ones.
|
||||
|
||||
Devices often answer a multi-question probe with partial record sets
|
||||
(ecobee returns only the PTR, #619); accumulate across probes so a thin
|
||||
response never discards previously learned SRV/TXT/A records.
|
||||
"""
|
||||
merged = {(r["name"], r["type"]): r for r in prev_recs}
|
||||
for r in new_recs:
|
||||
merged[(r["name"], r["type"])] = r
|
||||
return list(merged.values())
|
||||
|
||||
|
||||
def probe_device(ip, timeout=2.0):
|
||||
"""Return normalized records from a device's mDNS response."""
|
||||
qname = encode_name(SERVICE)
|
||||
@@ -83,7 +102,7 @@ def probe_device(ip, timeout=2.0):
|
||||
records = []
|
||||
for _ in range(an + ns):
|
||||
name, name_end = read_name(data, off)
|
||||
rtype, _rclass, ttl, rlen = struct.unpack(">HHIH", data[name_end:name_end + 10])
|
||||
rtype, _rclass, ttl, rlen = decode_rr_header(data, name_end)
|
||||
rec = {"name": name, "type": rtype, "ttl": min(ttl, 4500)}
|
||||
if rtype == 12: # PTR
|
||||
rec["ptr_target"] = read_name(data, name_end + 10)[0]
|
||||
@@ -147,7 +166,7 @@ def main():
|
||||
recs = probe_device(ip)
|
||||
with lock:
|
||||
if recs:
|
||||
state[ip] = recs
|
||||
state[ip] = merge_records(state.get(ip, []), recs)
|
||||
log.info("probe %s: %s", ip, f"{len(recs)} records" if recs else "no response")
|
||||
|
||||
def replay():
|
||||
|
||||
Reference in New Issue
Block a user