docs: реконструкция LAN-протокола FGLair, анализ legacy, планы fglair-core/HA/ESPHome
- PROTOCOL.md: полная спецификация (KDF, envelope/CBC-цепочка, local_reg, key exchange, commands.json 206/200, datapoint push, тайминги, таблицы свойств шаблонов A/B/F, облачный provisioning). Факты из APK помечены [APK], проверенные живыми экспериментами на AP-WC1E — [ПРОВЕРЕНО НА ПРИБОРЕ]: mDNS только :10276; лимит 2 LAN-сессии (3-я -> 503); принудительный re-key при возрасте сессии >= ~44с (единственный механизм самолечения десинхрона — 400/401 модуль игнорирует); записи не эхируются; delete_session освобождает слот. - LEGACY_ANALYSIS.md: причины рассинхрона (keep-alive 1200с вместо 10-15с + seq_no-фильтр) и перегрузки модуля; требования к новой реализации. - PLAN_CORE_LIBRARY.md: план C++20-библиотеки fglair-core (Linux + ESP-IDF), ключ lanip_key считается статичным, ротация — только ошибка + ручной перепровижининг. - PLAN_HOME_ASSISTANT.md: pyfglair (cffi wheel) + custom component, облачный provisioning только в config flow, ключ виден в диагностике для копирования в ESPHome. - PLAN_ESPHOME.md: external component, только ESP-IDF framework. - tools/probe_reference.py: эталонный клиент протокола (проверен на приборе end-to-end); tools/probe_mdns.py — mDNS-проба. - legacy/: снимок скрипта (апстрим gyro-labs/AirCon, вложенный клон не версионируется); apk/*.apk исключены из версионирования.
This commit is contained in:
83
tools/probe_mdns.py
Normal file
83
tools/probe_mdns.py
Normal file
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Пассивная mDNS-проба модуля кондиционера: A-запрос <DSN>.local
|
||||
на 224.0.0.251:5353 и :10276 (нестандартный порт Ayla из APK)."""
|
||||
import socket, struct, sys, time
|
||||
|
||||
DSN = "AC000W00REDACTED"
|
||||
HOST = DSN + ".local"
|
||||
|
||||
def qname(name):
|
||||
out = b""
|
||||
for lbl in name.split("."):
|
||||
out += bytes([len(lbl)]) + lbl.encode()
|
||||
return out + b"\x00"
|
||||
|
||||
def query_packet(txid):
|
||||
# standard query, RD=1, one question: A, class IN
|
||||
header = struct.pack(">HHHHHH", txid, 0x0100, 1, 0, 0, 0)
|
||||
return header + qname(HOST) + struct.pack(">HH", 1, 1)
|
||||
|
||||
def parse_name(buf, off):
|
||||
parts = []
|
||||
while True:
|
||||
l = buf[off]
|
||||
if l == 0:
|
||||
off += 1
|
||||
break
|
||||
if l & 0xC0 == 0xC0:
|
||||
ptr = struct.unpack(">H", buf[off:off+2])[0] & 0x3FFF
|
||||
parts.append(parse_name(buf, ptr)[0])
|
||||
off += 2
|
||||
break
|
||||
parts.append(buf[off+1:off+1+l].decode("latin1"))
|
||||
off += 1 + l
|
||||
return ".".join(parts), off
|
||||
|
||||
def parse_answers(buf):
|
||||
try:
|
||||
qd = struct.unpack(">H", buf[4:6])[0]
|
||||
an = struct.unpack(">H", buf[6:8])[0]
|
||||
off = 12
|
||||
for _ in range(qd):
|
||||
_, off = parse_name(buf, off)
|
||||
off += 4
|
||||
found = []
|
||||
for _ in range(an):
|
||||
name, off = parse_name(buf, off)
|
||||
rtype, rclass, ttl, rdlen = struct.unpack(">HHIH", buf[off:off+10])
|
||||
off += 10
|
||||
rdata = buf[off:off+rdlen]
|
||||
if rtype == 1 and rdlen == 4:
|
||||
ip = ".".join(str(b) for b in rdata)
|
||||
found.append((name, ip, rclass, ttl))
|
||||
off += rdlen
|
||||
return found
|
||||
except Exception as e:
|
||||
return [("parse-error", str(e), 0, 0)]
|
||||
|
||||
def probe(port, txid, wait=4.0):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
s.bind(("", 0))
|
||||
pkt = query_packet(txid)
|
||||
for _ in range(3):
|
||||
s.sendto(pkt, ("224.0.0.251", port))
|
||||
time.sleep(0.5)
|
||||
s.settimeout(wait)
|
||||
try:
|
||||
while True:
|
||||
data, addr = s.recvfrom(4096)
|
||||
print(f" [:{port}] from {addr}: {len(data)} bytes")
|
||||
for rec in parse_answers(data):
|
||||
print(f" {rec}")
|
||||
except socket.timeout:
|
||||
pass
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("mDNS A?", HOST)
|
||||
print("-- querying :5353")
|
||||
probe(5353, 0x1234)
|
||||
print("-- querying :10276")
|
||||
probe(10276, 0x1235)
|
||||
Reference in New Issue
Block a user