Separated handler name from MQTT client ID

This commit is contained in:
Jono Targett 2026-03-20 00:04:07 +10:30
parent a0421f07d0
commit d04a52be5d
4 changed files with 61 additions and 46 deletions

View File

@ -7,7 +7,7 @@ from dataclasses import dataclass
import aiohttp import aiohttp
from mqtthandler.command import command from mqtthandler.command import command
from mqtthandler.handler import MQTTConfig, MQTTHandler, task from mqtthandler.handler import MQTTHandler, task
from prometheus_client.parser import text_string_to_metric_families from prometheus_client.parser import text_string_to_metric_families
@dataclass @dataclass
@ -21,11 +21,10 @@ class MediaMTXConfig:
class MediaMTXHandler(MQTTHandler): class MediaMTXHandler(MQTTHandler):
def __init__( def __init__(
self, self,
mqtt_config: MQTTConfig, name: str,
handler_id: str,
mediamtx_config: MediaMTXConfig, mediamtx_config: MediaMTXConfig,
): ):
super().__init__(mqtt_config, handler_id) super().__init__(name)
self.config = mediamtx_config self.config = mediamtx_config
@task @task
@ -56,12 +55,9 @@ class MediaMTXHandler(MQTTHandler):
async def main(): async def main():
handler_id = f"mediamtx-{socket.gethostname()}" handler = MediaMTXHandler("mediamtx", MediaMTXConfig())
mqtt_config = MQTTConfig(host="127.0.0.1")
handler = MediaMTXHandler(mqtt_config, handler_id, MediaMTXConfig())
signal.signal(signal.SIGINT, lambda signum, frame: handler.stop()) signal.signal(signal.SIGINT, lambda signum, frame: handler.stop())
await handler.run() await handler.run("127.0.0.1", username="device", password="devicesecret")
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -4,6 +4,10 @@ import inspect
import paho import paho
import signal import signal
import json import json
import secrets
import os
import socket
from pathlib import Path
from enum import Enum, auto from enum import Enum, auto
from .command import ( from .command import (
@ -16,12 +20,31 @@ from .command import (
from .property import Property from .property import Property
def get_identifier(cache_path: Path) -> str:
"""
Determine an MQTT client ID using the following order:
1. Environment variable IDENTIFIER
2. Value stored in /tmp/<handler-name>.tmp
3. Generate a new random ID using secrets.token_urlsafe
The resulting client ID is written to /tmp/mqtt_client_id.tmp for future use.
"""
client_id = os.environ.get("IDENTIFIER", None)
if not client_id and cache_path.exists():
client_id = cache_path.read_text().strip()
elif not client_id:
client_id = generate_identifier()
cache_path.write_text(client_id)
return client_id
def generate_identifier() -> str:
return secrets.token_urlsafe(6)
class Status(Enum): class Status(Enum):
ONLINE = auto() ONLINE = auto()
OFFLINE = auto() OFFLINE = auto()
class MQTTHandler: class MQTTHandler:
DEVICE = "device" DEVICE = "device"
META = "meta" META = "meta"
@ -31,14 +54,15 @@ class MQTTHandler:
def __init__( def __init__(
self, self,
handler_id: str, name: str
): ):
self.handler_id = handler_id self.name = name
self.identifier = get_identifier(Path(f"/tmp/{self.name}.tmp"))
self.topic_base = f"{MQTTHandler.DEVICE}/{handler_id}" self.topic_base = lambda: f"{MQTTHandler.DEVICE}/{self.identifier}"
self.meta_topic = f"{self.topic_base}/{MQTTHandler.META}" self.meta_topic = lambda: f"{self.topic_base()}/{MQTTHandler.META}"
self.command_topic = f"{self.topic_base}/{MQTTHandler.COMMAND}" self.command_topic = lambda: f"{self.topic_base()}/{MQTTHandler.COMMAND}"
self.property_topic = f"{self.topic_base}/{MQTTHandler.PROPERTY}" self.property_topic = lambda: f"{self.topic_base()}/{MQTTHandler.PROPERTY}"
self._shutdown_event = asyncio.Event() self._shutdown_event = asyncio.Event()
@ -53,13 +77,13 @@ class MQTTHandler:
await self._properties[name](value, **kwargs) await self._properties[name](value, **kwargs)
else: else:
#print(f"Warning: proeprty {name} is unregistered") #print(f"Warning: proeprty {name} is unregistered")
await self._publish(f"{self.property_topic}/{name}", value, **kwargs) await self._publish(f"{self.property_topic()}/{name}", value, **kwargs)
async def register_property( async def register_property(
self, name: str, description: str | None = None, schema: dict | None = None self, name: str, description: str | None = None, schema: dict | None = None
): ):
property = self._register_property( property = self._register_property(
f"{self.property_topic}/{name}", description, schema f"{self.property_topic()}/{name}", description, schema
) )
self._properties[name] = property self._properties[name] = property
@ -92,7 +116,7 @@ class MQTTHandler:
**command.additional_properties, **command.additional_properties,
}.items(): }.items():
await self._mqtt_client.publish( await self._mqtt_client.publish(
f"{self.command_topic}/{command.name}/${k}", f"{self.command_topic()}/{command.name}/${k}",
str(v), str(v),
qos=1, qos=1,
retain=True, retain=True,
@ -103,7 +127,7 @@ class MQTTHandler:
await self._register_commands() await self._register_commands()
self._meta[MQTTHandler.STATUS] = await self._register_property( self._meta[MQTTHandler.STATUS] = await self._register_property(
f"{self.meta_topic}/{MQTTHandler.STATUS}", f"{self.meta_topic()}/{MQTTHandler.STATUS}",
"Indicates the status of the device.", "Indicates the status of the device.",
{"type": "string", "enum": list(Status.__members__.keys())}, {"type": "string", "enum": list(Status.__members__.keys())},
) )
@ -111,6 +135,10 @@ class MQTTHandler:
self, json.dumps(Status.ONLINE.name), qos=1, retain=True self, json.dumps(Status.ONLINE.name), qos=1, retain=True
) )
await self._publish(f"{self.meta_topic()}/name", self.name, qos=1, retain=True)
await self._publish(f"{self.meta_topic()}/type", type(self).__name__, qos=1, retain=True)
await self._publish(f"{self.meta_topic()}/host", socket.gethostname(), qos=1, retain=True)
async def _execute_command( async def _execute_command(
self, self,
command_name: str, command_name: str,
@ -146,14 +174,14 @@ class MQTTHandler:
await respond(False, "Unexpected error") await respond(False, "Unexpected error")
async def _command_executor(self): async def _command_executor(self):
await self._mqtt_client.subscribe(f"{self.command_topic}/+") await self._mqtt_client.subscribe(f"{self.command_topic()}/+")
async for message in self._mqtt_client.messages: async for message in self._mqtt_client.messages:
topic = str(message.topic) topic = str(message.topic)
payload = message.payload.decode("utf-8") payload = message.payload.decode("utf-8")
if topic.startswith(self.command_topic): if topic.startswith(self.command_topic()):
command_name = topic.removeprefix(f"{self.command_topic}/") command_name = topic.removeprefix(f"{self.command_topic()}/")
await self._execute_command(command_name, payload, message.properties) await self._execute_command(command_name, payload, message.properties)
async def _shutdown_watcher(self): async def _shutdown_watcher(self):
@ -170,7 +198,7 @@ class MQTTHandler:
INTERVAL = 5 INTERVAL = 5
will = aiomqtt.Will( will = aiomqtt.Will(
topic=f"{self.meta_topic}/{MQTTHandler.STATUS}", topic=f"{self.meta_topic()}/{MQTTHandler.STATUS}",
payload=json.dumps(Status.OFFLINE.name), payload=json.dumps(Status.OFFLINE.name),
qos=1, qos=1,
retain=True, retain=True,
@ -182,7 +210,7 @@ class MQTTHandler:
host, host,
protocol=paho.mqtt.client.MQTTv5, protocol=paho.mqtt.client.MQTTv5,
will=will, will=will,
identifier=self.handler_id, identifier=self.identifier,
**kwargs, **kwargs,
) as client: ) as client:
self._mqtt_client = client self._mqtt_client = client
@ -207,7 +235,7 @@ class MQTTHandler:
except aiomqtt.MqttError as e: except aiomqtt.MqttError as e:
print( print(
f"[{self.handler_id}] MQTT connection error: {e}. Reconnecting in {INTERVAL}s..." f"MQTT connection error: {e}. Reconnecting in {INTERVAL}s..."
) )
await asyncio.sleep(INTERVAL) await asyncio.sleep(INTERVAL)

View File

@ -6,24 +6,22 @@ import asyncio
from dataclasses import dataclass from dataclasses import dataclass
from mqtthandler.command import command from mqtthandler.command import command
from mqtthandler.handler import MQTTConfig, MQTTHandler, task from mqtthandler.handler import MQTTHandler, task
from streamer.fileradio import FileRadio from streamer.fileradio import FileRadio
class RadioHandler(MQTTHandler): class RadioHandler(MQTTHandler):
def __init__( def __init__(
self, self,
mqtt_config: MQTTConfig, name: str,
handler_id: str,
): ):
super().__init__(mqtt_config, handler_id) super().__init__(name)
self.radio = FileRadio("./data/StarWars60.mp3", name)
self.radio = FileRadio("./data/StarWars60.mp3", handler_id)
@task @task
async def publish_stream_path(self): async def publish_stream_path(self):
await self.set_property("path", self.radio.stream_path(), qos=1, qos=True) await self.set_property("path", self.radio.stream_path(), qos=1, retain=True)
await self.set_property("file", self.radio.path, qos=1, qos=True) await self.set_property("file", self.radio.path, qos=1, retain=True)
@command({"type": "object"}, "Start the radio stream.") @command({"type": "object"}, "Start the radio stream.")
async def start(self, args): async def start(self, args):
@ -48,12 +46,9 @@ class RadioHandler(MQTTHandler):
await self.publish_stream_path() await self.publish_stream_path()
async def main(): async def main():
handler_id = f"radio-{socket.gethostname()}" handler = RadioHandler("radio")
mqtt_config = MQTTConfig(host="127.0.0.1")
handler = RadioHandler(mqtt_config, handler_id)
signal.signal(signal.SIGINT, lambda signum, frame: handler.stop()) signal.signal(signal.SIGINT, lambda signum, frame: handler.stop())
await handler.run() await handler.run("127.0.0.1", username="device", password="devicesecret")
if __name__ == "__main__": if __name__ == "__main__":

10
ubx.py
View File

@ -2,13 +2,11 @@
import asyncio import asyncio
import aioserial import aioserial
import aiomqtt
import pyubx2 import pyubx2
import io import io
import logging import logging
import aioserial import aioserial
import asyncio import asyncio
import socket
import signal import signal
from mqtthandler.command import command from mqtthandler.command import command
@ -47,10 +45,10 @@ class UBXAsyncParser:
class UBXHandler(MQTTHandler): class UBXHandler(MQTTHandler):
def __init__( def __init__(
self, self,
handler_id: str, name: str,
serial_port: aioserial.AioSerial, serial_port: aioserial.AioSerial,
): ):
super().__init__(handler_id) super().__init__(name)
self.serial_port = serial_port self.serial_port = serial_port
@task @task
@ -173,10 +171,8 @@ class UBXHandler(MQTTHandler):
async def main(): async def main():
handler_id = f"example-gps-{socket.gethostname()}"
handler = UBXHandler( handler = UBXHandler(
handler_id, "example-gps",
aioserial.AioSerial( aioserial.AioSerial(
port="/tmp/ttyV0", port="/tmp/ttyV0",
baudrate=115200, baudrate=115200,