Implement Server-Sent-Events API #3

Merged
lilly merged 5 commits from lilly/sse into main 2026-05-19 14:54:49 +02:00
2 changed files with 53 additions and 15 deletions
Showing only changes of commit 4103c0ca5f - Show all commits

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

lilly 2026-05-19 14:10:47 +02:00
Signed by: lilly
SSH key fingerprint: SHA256:y9T5GFw2A20WVklhetIxG1+kcg/Ce0shnQmbu1LQ37g

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 from aiohttp import ClientSession, BasicAuth, TCPConnector
import logging import logging
import asyncio import asyncio
@ -70,6 +70,8 @@ LockData = List[Tuple[CCUDeviceInfo, List[Tuple[CCUChannelInfo, List[CCUParamInf
class CCUJackClient: class CCUJackClient:
base_uri: str base_uri: str
locks: LockData locks: LockData
param_values: Dict[str, Any]
task_process_messages: asyncio.Task
def __init__(self, base_uri: str, auth: BasicAuth, mqtt_conn: str): def __init__(self, base_uri: str, auth: BasicAuth, mqtt_conn: str):
self.http = ClientSession( self.http = ClientSession(
@ -78,17 +80,21 @@ class CCUJackClient:
raise_for_status=True, raise_for_status=True,
connector=TCPConnector(ssl=False), connector=TCPConnector(ssl=False),
) )
self.locks = None
self.mqtt = AsyncMqttClient(mqtt_conn, auth.login, auth.password) 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): async def connect_mqtt(self):
await self.mqtt.connect() 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): async def close_connections(self):
await asyncio.gather( await asyncio.gather(self.mqtt.disconnect(), self.http.close())
self.mqtt.disconnect(), self.task_process_messages.cancel()
self.http.close() self.task_process_messages = None
)
async def find_locks(self): async def find_locks(self):
logger.debug("Inspecting lock devices present in CCUJack") logger.debug("Inspecting lock devices present in CCUJack")
@ -112,10 +118,30 @@ class CCUJackClient:
for i_lock, lock_channels in self.locks: for i_lock, lock_channels in self.locks:
for i_channel, channel_params in lock_channels: for i_channel, channel_params in lock_channels:
for i_param in channel_params: for i_param in channel_params:
mqtt_topics.add(f"device/status/{i_lock.address}/{i_channel.index}/{i_param.id}") mqtt_topics.add(
# await self.mqtt.update_subscriptions(mqtt_topics) 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) logger.debug("Querying parameter value from '%s'", address)
async with self.http.get(f"/device/{address}/~pv") as resp: async with self.http.get(f"/device/{address}/~pv") as resp:
return CCUValue.model_validate(await resp.json()) return CCUValue.model_validate(await resp.json())

View file

@ -62,16 +62,22 @@ class AsyncMqttClient:
looper: AsyncLooper looper: AsyncLooper
client: mqtt.Client client: mqtt.Client
active_subscriptions: Set[str] active_subscriptions: Set[str]
messages: asyncio.Queue
def __init__(self, connection_string: str, username: str, password: str): def __init__(self, connection_string: str, username: str, password: str):
self.connection_string = connection_string self.connection_string = connection_string
self.active_subscriptions = set() self.active_subscriptions = set()
self.messages = asyncio.Queue()
self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="dooris") self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="dooris")
self.client.username = username self.client.username = username
self.client.password = password self.client.password = password
self.client.on_connect = self.on_connect 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_message = self.on_message
self.client.on_disconnect = self.on_disconnect 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( def on_connect(
self, self,
@ -84,6 +90,12 @@ class AsyncMqttClient:
logger.debug(f"mqtt client connected with message '{reason_code}'") logger.debug(f"mqtt client connected with message '{reason_code}'")
self.fut_connected.set_result(None) 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( def on_disconnect(
self, self,
client: mqtt.Client, client: mqtt.Client,
@ -97,18 +109,19 @@ class AsyncMqttClient:
self.fut_disconnect.set_result(None) self.fut_disconnect.set_result(None)
def on_message(self, client: mqtt.Client, userdata: Any, msg: mqtt.MQTTMessage): def on_message(self, client: mqtt.Client, userdata: Any, msg: mqtt.MQTTMessage):
logger.debug("mqtt client got message") self.messages.put_nowait(msg)
print("msg", type(msg), msg)
def on_subscribe(self, client, userdata, mid, reason_code, properties): def on_subscribe(self, client, userdata, mid, reason_code, properties):
logger.debug(f"mqtt client subscribed to topics with message '{reason_code}'") logger.debug(f"mqtt client subscribed to topics with message '{reason_code}'")
self.fut_subscribe.set_result(None) self.fut_subscribe.set_result(None)
def on_unsubscribe(self, client, userdata, mid, reason_code, properties): 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) 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
""" """
@ -116,7 +129,6 @@ class AsyncMqttClient:
to_add = topics.difference(self.active_subscriptions) to_add = topics.difference(self.active_subscriptions)
if to_add: if to_add:
logger.info(f"mqtt client subscribing to topics {', '.join(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.fut_subscribe = asyncio.get_running_loop().create_future()
self.client.subscribe([(i, qos) for i in to_add]) self.client.subscribe([(i, qos) for i in to_add])
await self.fut_subscribe await self.fut_subscribe