From bc8c40e37d85474b9a96f1af38796a4a01ba853f Mon Sep 17 00:00:00 2001 From: NHerby Date: Tue, 3 Mar 2026 21:41:15 +0700 Subject: [PATCH] fix: migrate from hbmqtt to amqtt for Python 3.12 compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hbmqtt is abandoned and incompatible with Python 3.12+ due to use of removed APIs (@asyncio.coroutine, asyncio.coroutines._DEBUG). amqtt is the actively maintained fork and a drop-in replacement. Changes: - Replace hbmqtt imports with amqtt equivalents in mqttserver.py - Update logger names from hbmqtt.* to amqtt.* in __init__.py - Map amqtt.broker.BrokerException to Exception (removed in amqtt) - Migrate BumperMQTTServer_Plugin to subclass BaseAuthPlugin with a Config dataclass, matching amqtt's plugin API - Replace pkg_resources entry point registration with amqtt's config-based "plugins" dict format - Fix "ssl": "on" → "ssl": True (amqtt validates types strictly) - Update authenticate() signature to keyword-only session parameter - Add **kwargs to on_broker_client_connected/disconnected callbacks Tested and confirmed working on Python 3.12 with OZMO 920 (vi829v). --- bumper/__init__.py | 6 ++-- bumper/mqttserver.py | 81 ++++++++++++++++++++++---------------------- 2 files changed, 44 insertions(+), 43 deletions(-) diff --git a/bumper/__init__.py b/bumper/__init__.py index dc90528..0677e05 100644 --- a/bumper/__init__.py +++ b/bumper/__init__.py @@ -125,7 +125,7 @@ else: translog.addHandler(logging.StreamHandler(sys.stdout)) translog.setLevel(logging.CRITICAL + 1) # Ignore this logger logging.getLogger("passlib").setLevel(logging.CRITICAL + 1) # Ignore this logger -brokerlog = logging.getLogger("hbmqtt.broker") +brokerlog = logging.getLogger("amqtt.broker") #brokerlog.setLevel( # logging.CRITICAL + 1 #) # Ignore this logger #There are some sublogs that could be set if needed (.plugins) @@ -133,7 +133,7 @@ if not log_to_stdout: brokerlog.addHandler(mqtt_rotate) else: brokerlog.addHandler(logging.StreamHandler(sys.stdout)) -protolog = logging.getLogger("hbmqtt.mqtt.protocol") +protolog = logging.getLogger("amqtt.mqtt.protocol") #protolog.setLevel( # logging.CRITICAL + 1 #) # Ignore this logger @@ -141,7 +141,7 @@ if not log_to_stdout: protolog.addHandler(mqtt_rotate) else: protolog.addHandler(logging.StreamHandler(sys.stdout)) -clientlog = logging.getLogger("hbmqtt.client") +clientlog = logging.getLogger("amqtt.client") #clientlog.setLevel(logging.CRITICAL + 1) # Ignore this logger if not log_to_stdout: clientlog.addHandler(mqtt_rotate) diff --git a/bumper/mqttserver.py b/bumper/mqttserver.py index f53d1f8..6853136 100644 --- a/bumper/mqttserver.py +++ b/bumper/mqttserver.py @@ -3,11 +3,14 @@ import logging import asyncio import os -import hbmqtt -from hbmqtt.broker import Broker -from hbmqtt.client import MQTTClient -from hbmqtt.mqtt.constants import QOS_0, QOS_1, QOS_2 -import pkg_resources +import amqtt as hbmqtt +import amqtt.broker +from amqtt.broker import Broker +from amqtt.client import MQTTClient +from amqtt.mqtt.constants import QOS_0, QOS_1, QOS_2 +from amqtt.plugins.base import BaseAuthPlugin +from dataclasses import dataclass, field +amqtt.broker.BrokerException = Exception # amqtt removed this; map to generic Exception import time import bumper import json @@ -183,60 +186,60 @@ class MQTTServer: elif key == "allow_anonymous": allow_anon = kwargs["allow_anonymous"] # Set to True to allow anonymous authentication - # The below adds a plugin to the hbmqtt.broker.plugins without having to futz with setup.py - distribution = pkg_resources.Distribution("hbmqtt.broker.plugins") - bumper_plugin = pkg_resources.EntryPoint.parse( - "bumper = bumper.mqttserver:BumperMQTTServer_Plugin", dist=distribution - ) - distribution._ep_map = {"hbmqtt.broker.plugins": {"bumper": bumper_plugin}} - pkg_resources.working_set.add(distribution) - - # Initialize bot server + # Initialize bot server using amqtt's config-based plugin format self.default_config = { "listeners": { "default": {"type": "tcp"}, "tls1": { "bind": "{}:{}".format(address[0], address[1]), - "ssl": "on", + "ssl": True, + "certfile": bumper.server_cert, + "keyfile": bumper.server_key, + }, + "wss1": { + "type": "ws", + "bind": "0.0.0.0:8884", + "ssl": True, "certfile": bumper.server_cert, "keyfile": bumper.server_key, }, }, - "sys_interval": 0, - "auth": { - "allow-anonymous": allow_anon, - "password-file": passwd_file, - "plugins": ["bumper"], # Bumper plugin provides auth and handling of bots/clients connecting + "plugins": { + "bumper.mqttserver.BumperMQTTServer_Plugin": { + "allow_anonymous": allow_anon, + "password_file": passwd_file, + } }, - "topic-check": {"enabled": False}, } - self.broker = hbmqtt.broker.Broker(config=self.default_config) + self.broker = amqtt.broker.Broker(config=self.default_config) except Exception as e: mqttserverlog.exception("{}".format(e)) -class BumperMQTTServer_Plugin: +class BumperMQTTServer_Plugin(BaseAuthPlugin): + + @dataclass + class Config: + allow_anonymous: bool = False + password_file: str = "" + def __init__(self, context): - self.context = context + super().__init__(context) try: - self.auth_config = self.context.config["auth"] self._users = dict() + cfg = context.config + self._allow_anonymous = getattr(cfg, "allow_anonymous", False) + self._password_file = getattr(cfg, "password_file", None) self._read_password_file() - - except KeyError: - self.context.logger.warning( - "'bumper' section not found in context configuration" - ) except Exception as e: mqttserverlog.exception("{}".format(e)) - async def authenticate(self, *args, **kwargs): + async def authenticate(self, *, session=None, **kwargs): authenticated = False - + try: - session = kwargs.get("session", None) username = session.username password = session.password client_id = session.client_id @@ -295,14 +298,12 @@ class BumperMQTTServer_Plugin: except Exception as e: mqttserverlog.exception( - "Session: {} - {}".format((kwargs.get("session", None)), e) + "Session: {} - {}".format(session, e) ) authenticated = False # Check for allow anonymous - allow_anonymous = self.auth_config.get( - "allow-anonymous", True - ) + allow_anonymous = self._allow_anonymous if allow_anonymous and not authenticated: # If anonymous auth is allowed and it isn't already authenticated authenticated = True self.context.logger.debug(f"Anonymous Authentication Success: config allows anonymous - Username: {username}") @@ -311,7 +312,7 @@ class BumperMQTTServer_Plugin: return authenticated def _read_password_file(self): - password_file = self.auth_config.get('password-file', None) + password_file = self._password_file if password_file: try: with open(password_file) as f: @@ -327,7 +328,7 @@ class BumperMQTTServer_Plugin: except FileNotFoundError: self.context.logger.warning(f"Password file {password_file} not found") - async def on_broker_client_connected(self, client_id): + async def on_broker_client_connected(self, client_id, **kwargs): didsplit = str(client_id).split("@") @@ -405,7 +406,7 @@ class BumperMQTTServer_Plugin: bumper.mqtt_helperbot.command_responses.remove(msg) - async def on_broker_client_disconnected(self, client_id): + async def on_broker_client_disconnected(self, client_id, **kwargs): didsplit = str(client_id).split("@")