api: process mqtt messages to keep a current local lock state

This commit is contained in:
lilly 2026-05-19 14:10:47 +02:00
commit 4103c0ca5f
Signed by: lilly
SSH key fingerprint: SHA256:y9T5GFw2A20WVklhetIxG1+kcg/Ce0shnQmbu1LQ37g
2 changed files with 53 additions and 15 deletions

View file

@ -1,4 +1,4 @@
from typing import List, Tuple, Optional, Any
from typing import List, Tuple, Optional, Any, Dict
from aiohttp import ClientSession, BasicAuth, TCPConnector
import logging
import asyncio
@ -70,6 +70,8 @@ LockData = List[Tuple[CCUDeviceInfo, List[Tuple[CCUChannelInfo, List[CCUParamInf
class CCUJackClient:
base_uri: str
locks: LockData
param_values: Dict[str, Any]
task_process_messages: asyncio.Task
def __init__(self, base_uri: str, auth: BasicAuth, mqtt_conn: str):
self.http = ClientSession(
@ -78,17 +80,21 @@ class CCUJackClient:
raise_for_status=True,
connector=TCPConnector(ssl=False),
)
self.locks = None
self.mqtt = AsyncMqttClient(mqtt_conn, auth.login, auth.password)
self.locks = None
self.param_values = dict()
self.task_process_messages = None
async def connect_mqtt(self):
await self.mqtt.connect()
self.task_process_messages = asyncio.get_running_loop().create_task(
self.process_mqt_messages(), name="process-mqtt-messages"
)
async def close_connections(self):
await asyncio.gather(
self.mqtt.disconnect(),
self.http.close()
)
await asyncio.gather(self.mqtt.disconnect(), self.http.close())
self.task_process_messages.cancel()
self.task_process_messages = None
async def find_locks(self):
logger.debug("Inspecting lock devices present in CCUJack")
@ -112,10 +118,30 @@ class CCUJackClient:
for i_lock, lock_channels in self.locks:
for i_channel, channel_params in lock_channels:
for i_param in channel_params:
mqtt_topics.add(f"device/status/{i_lock.address}/{i_channel.index}/{i_param.id}")
# await self.mqtt.update_subscriptions(mqtt_topics)
mqtt_topics.add(
f"device/status/{i_lock.address}/{i_channel.index}/{i_param.id}"
)
await self.mqtt.update_subscriptions(mqtt_topics)
async def process_mqt_messages(self):
while True:
try:
msg = await self.mqtt.messages.get()
param_name = msg.topic.removeprefix("device/status/")
param_value = CCUValue.model_validate_json(msg.payload)
logger.debug(
f"Got new value from MQTT for parameter {param_name}: {param_value}"
)
self.param_values[param_name] = param_value
except Exception as e:
logger.exception(f"could not process incoming mqtt message: {e}")
async def query_param_value(self, address: str) -> CCUValue:
if address in self.param_values:
return self.param_values[address]
async def query_param_value(self, address: str):
logger.debug("Querying parameter value from '%s'", address)
async with self.http.get(f"/device/{address}/~pv") as resp:
return CCUValue.model_validate(await resp.json())

View file

@ -62,16 +62,22 @@ class AsyncMqttClient:
looper: AsyncLooper
client: mqtt.Client
active_subscriptions: Set[str]
messages: asyncio.Queue
def __init__(self, connection_string: str, username: str, password: str):
self.connection_string = connection_string
self.active_subscriptions = set()
self.messages = asyncio.Queue()
self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="dooris")
self.client.username = username
self.client.password = password
self.client.on_connect = self.on_connect
self.client.on_connect_fail = self.on_connect_fail
self.client.on_message = self.on_message
self.client.on_disconnect = self.on_disconnect
self.client.on_subscribe = self.on_subscribe
self.client.on_unsubscribe = self.on_unsubscribe
self.client.on_disconnect = self.on_disconnect
def on_connect(
self,
@ -84,6 +90,12 @@ class AsyncMqttClient:
logger.debug(f"mqtt client connected with message '{reason_code}'")
self.fut_connected.set_result(None)
def on_connect_fail(self, client, userdata):
logger.error("mqtt client could not connect to broker")
self.fut_connected.set_exception(
Exception("mqtt client could not connect to broker")
)
def on_disconnect(
self,
client: mqtt.Client,
@ -97,26 +109,26 @@ class AsyncMqttClient:
self.fut_disconnect.set_result(None)
def on_message(self, client: mqtt.Client, userdata: Any, msg: mqtt.MQTTMessage):
logger.debug("mqtt client got message")
print("msg", type(msg), msg)
self.messages.put_nowait(msg)
def on_subscribe(self, client, userdata, mid, reason_code, properties):
logger.debug(f"mqtt client subscribed to topics with message '{reason_code}'")
self.fut_subscribe.set_result(None)
def on_unsubscribe(self, client, userdata, mid, reason_code, properties):
logger.debug(f"mqtt client unsubscribed from topics with message '{reason_code}'")
logger.debug(
f"mqtt client unsubscribed from topics with message '{reason_code}'"
)
self.fut_unsubscribe.set_result(None)
async def update_subscriptions(self, topics: Iterable[str]):
async def update_subscriptions(self, topics: Iterable[str], qos: int = 1):
"""
Update MQTT subscriptions so that the client is subscribed to exactly the given list of topics
Update MQTT subscriptions so that the client is subscribed to exactly the given list of topics
"""
to_add = topics.difference(self.active_subscriptions)
if to_add:
logger.info(f"mqtt client subscribing to topics {', '.join(to_add)}")
qos = 2
self.fut_subscribe = asyncio.get_running_loop().create_future()
self.client.subscribe([(i, qos) for i in to_add])
await self.fut_subscribe