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:
92
legacy/mqtt_client.py
Normal file
92
legacy/mqtt_client.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from dataclasses import fields
|
||||
import enum
|
||||
import logging
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
from .aircon import Device
|
||||
from .properties import AcWorkMode, FglOperationMode
|
||||
|
||||
|
||||
class MqttClient(mqtt.Client):
|
||||
|
||||
def __init__(self, client_id: str, mqtt_topics: dict, devices: [Device]):
|
||||
super().__init__(client_id=client_id, clean_session=True)
|
||||
self._mqtt_topics = mqtt_topics
|
||||
self._devices = devices
|
||||
|
||||
self.on_connect = self.mqtt_on_connect
|
||||
self.on_message = self.mqtt_on_message
|
||||
|
||||
def mqtt_on_connect(self, client: mqtt.Client, userdata, flags, rc):
|
||||
for device in self._devices:
|
||||
topics_fmt = [(self._mqtt_topics['sub'].format(device.mac_address, data_field.name), 0)
|
||||
for data_field in fields(device.get_all_properties())]
|
||||
logging.debug(f"Subscribing to topics{topics_fmt} for device {device}")
|
||||
client.subscribe(topics_fmt)
|
||||
# Subscribe to subscription updates.
|
||||
client.subscribe('$SYS/broker/log/M/subscribe/#')
|
||||
|
||||
# Publish current status of all properties for available devices.
|
||||
for device in self._devices:
|
||||
if device.available:
|
||||
for prop_name in fields(device.get_all_properties()):
|
||||
self.mqtt_publish_update(device.mac_address,
|
||||
prop_name,
|
||||
device.get_property(prop_name),
|
||||
retain=False)
|
||||
|
||||
def mqtt_on_message(self, client: mqtt.Client, userdata, message: mqtt.MQTTMessage):
|
||||
logging.info('MQTT message Topic: {}, Payload {}'.format(message.topic, message.payload))
|
||||
if message.topic.startswith('$SYS/broker/log/M/subscribe'):
|
||||
return self.mqtt_on_subscribe(message.payload)
|
||||
mac_address = message.topic.rsplit('/', 3)[1]
|
||||
prop_name = message.topic.rsplit('/', 3)[2]
|
||||
payload = message.payload.decode('utf-8')
|
||||
if prop_name == 't_work_mode':
|
||||
if payload == 'fan_only':
|
||||
payload = 'FAN'
|
||||
|
||||
for device in self._devices:
|
||||
if device.mac_address != mac_address:
|
||||
continue
|
||||
chosen_device = device
|
||||
|
||||
try:
|
||||
chosen_device.queue_command(prop_name, payload.upper())
|
||||
except Exception:
|
||||
logging.exception('Failed to parse value {} for property {}'.format(
|
||||
payload.upper(), prop_name))
|
||||
|
||||
def mqtt_on_subscribe(self, payload: bytes):
|
||||
# The last segment in the space delimited string is the topic.
|
||||
topic = payload.decode('utf-8').rsplit(' ', 1)[-1]
|
||||
if topic not in self._mqtt_topics['pub']:
|
||||
return
|
||||
mac_address = topic.rsplit('/', 3)[1]
|
||||
prop_name = topic.rsplit('/', 3)[2]
|
||||
|
||||
for device in self._devices:
|
||||
if device.mac_address != mac_address:
|
||||
continue
|
||||
chosen_device = device
|
||||
|
||||
self.mqtt_publish_update(chosen_device.mac_address,
|
||||
prop_name,
|
||||
chosen_device.get_property(prop_name),
|
||||
retain=False)
|
||||
|
||||
def mqtt_publish_update(self,
|
||||
mac_address: str,
|
||||
property_name: str,
|
||||
value,
|
||||
retain: bool = False) -> None:
|
||||
if isinstance(value, enum.Enum):
|
||||
payload = 'fan_only' if (value is AcWorkMode.FAN or
|
||||
value is FglOperationMode.FAN_ONLY) else value.name.lower()
|
||||
else:
|
||||
payload = str(value)
|
||||
topic = self._mqtt_topics['pub'].format(mac_address, property_name)
|
||||
logging.info('Sending MQTT update Topic: {}, Payload {}'.format(topic, payload))
|
||||
self.publish(topic,
|
||||
payload=payload.encode('utf-8'),
|
||||
retain=retain)
|
||||
Reference in New Issue
Block a user