diff --git a/sbapp/mqtt/__init__.py b/sbapp/mqtt/__init__.py deleted file mode 100644 index 9372c8f..0000000 --- a/sbapp/mqtt/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -__version__ = "2.1.1.dev0" - - -class MQTTException(Exception): - pass diff --git a/sbapp/mqtt/client.py b/sbapp/mqtt/client.py deleted file mode 100644 index 4ccc869..0000000 --- a/sbapp/mqtt/client.py +++ /dev/null @@ -1,5004 +0,0 @@ -# Copyright (c) 2012-2019 Roger Light and others -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Eclipse Public License v2.0 -# and Eclipse Distribution License v1.0 which accompany this distribution. -# -# The Eclipse Public License is available at -# http://www.eclipse.org/legal/epl-v20.html -# and the Eclipse Distribution License is available at -# http://www.eclipse.org/org/documents/edl-v10.php. -# -# Contributors: -# Roger Light - initial API and implementation -# Ian Craggs - MQTT V5 support -""" -This is an MQTT client module. MQTT is a lightweight pub/sub messaging -protocol that is easy to implement and suitable for low powered devices. -""" -from __future__ import annotations - -import base64 -import collections -import errno -import hashlib -import logging -import os -import platform -import select -import socket -import string -import struct -import threading -import time -import urllib.parse -import urllib.request -import uuid -import warnings -from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, NamedTuple, Sequence, Tuple, Union, cast - -from paho.mqtt.packettypes import PacketTypes - -from .enums import CallbackAPIVersion, ConnackCode, LogLevel, MessageState, MessageType, MQTTErrorCode, MQTTProtocolVersion, PahoClientMode, _ConnectionState -from .matcher import MQTTMatcher -from .properties import Properties -from .reasoncodes import ReasonCode, ReasonCodes -from .subscribeoptions import SubscribeOptions - -try: - from typing import Literal -except ImportError: - from typing_extensions import Literal # type: ignore - -if TYPE_CHECKING: - try: - from typing import TypedDict # type: ignore - except ImportError: - from typing_extensions import TypedDict - - try: - from typing import Protocol # type: ignore - except ImportError: - from typing_extensions import Protocol # type: ignore - - class _InPacket(TypedDict): - command: int - have_remaining: int - remaining_count: list[int] - remaining_mult: int - remaining_length: int - packet: bytearray - to_process: int - pos: int - - - class _OutPacket(TypedDict): - command: int - mid: int - qos: int - pos: int - to_process: int - packet: bytes - info: MQTTMessageInfo | None - - class SocketLike(Protocol): - def recv(self, buffer_size: int) -> bytes: - ... - def send(self, buffer: bytes) -> int: - ... - def close(self) -> None: - ... - def fileno(self) -> int: - ... - def setblocking(self, flag: bool) -> None: - ... - - -try: - import ssl -except ImportError: - ssl = None # type: ignore[assignment] - - -try: - import socks # type: ignore[import-untyped] -except ImportError: - socks = None # type: ignore[assignment] - - -try: - # Use monotonic clock if available - time_func = time.monotonic -except AttributeError: - time_func = time.time - -try: - import dns.resolver - - HAVE_DNS = True -except ImportError: - HAVE_DNS = False - - -if platform.system() == 'Windows': - EAGAIN = errno.WSAEWOULDBLOCK # type: ignore[attr-defined] -else: - EAGAIN = errno.EAGAIN - -# Avoid linter complain. We kept importing it as ReasonCodes (plural) for compatibility -_ = ReasonCodes - -# Keep copy of enums values for compatibility. -CONNECT = MessageType.CONNECT -CONNACK = MessageType.CONNACK -PUBLISH = MessageType.PUBLISH -PUBACK = MessageType.PUBACK -PUBREC = MessageType.PUBREC -PUBREL = MessageType.PUBREL -PUBCOMP = MessageType.PUBCOMP -SUBSCRIBE = MessageType.SUBSCRIBE -SUBACK = MessageType.SUBACK -UNSUBSCRIBE = MessageType.UNSUBSCRIBE -UNSUBACK = MessageType.UNSUBACK -PINGREQ = MessageType.PINGREQ -PINGRESP = MessageType.PINGRESP -DISCONNECT = MessageType.DISCONNECT -AUTH = MessageType.AUTH - -# Log levels -MQTT_LOG_INFO = LogLevel.MQTT_LOG_INFO -MQTT_LOG_NOTICE = LogLevel.MQTT_LOG_NOTICE -MQTT_LOG_WARNING = LogLevel.MQTT_LOG_WARNING -MQTT_LOG_ERR = LogLevel.MQTT_LOG_ERR -MQTT_LOG_DEBUG = LogLevel.MQTT_LOG_DEBUG -LOGGING_LEVEL = { - LogLevel.MQTT_LOG_DEBUG: logging.DEBUG, - LogLevel.MQTT_LOG_INFO: logging.INFO, - LogLevel.MQTT_LOG_NOTICE: logging.INFO, # This has no direct equivalent level - LogLevel.MQTT_LOG_WARNING: logging.WARNING, - LogLevel.MQTT_LOG_ERR: logging.ERROR, -} - -# CONNACK codes -CONNACK_ACCEPTED = ConnackCode.CONNACK_ACCEPTED -CONNACK_REFUSED_PROTOCOL_VERSION = ConnackCode.CONNACK_REFUSED_PROTOCOL_VERSION -CONNACK_REFUSED_IDENTIFIER_REJECTED = ConnackCode.CONNACK_REFUSED_IDENTIFIER_REJECTED -CONNACK_REFUSED_SERVER_UNAVAILABLE = ConnackCode.CONNACK_REFUSED_SERVER_UNAVAILABLE -CONNACK_REFUSED_BAD_USERNAME_PASSWORD = ConnackCode.CONNACK_REFUSED_BAD_USERNAME_PASSWORD -CONNACK_REFUSED_NOT_AUTHORIZED = ConnackCode.CONNACK_REFUSED_NOT_AUTHORIZED - -# Message state -mqtt_ms_invalid = MessageState.MQTT_MS_INVALID -mqtt_ms_publish = MessageState.MQTT_MS_PUBLISH -mqtt_ms_wait_for_puback = MessageState.MQTT_MS_WAIT_FOR_PUBACK -mqtt_ms_wait_for_pubrec = MessageState.MQTT_MS_WAIT_FOR_PUBREC -mqtt_ms_resend_pubrel = MessageState.MQTT_MS_RESEND_PUBREL -mqtt_ms_wait_for_pubrel = MessageState.MQTT_MS_WAIT_FOR_PUBREL -mqtt_ms_resend_pubcomp = MessageState.MQTT_MS_RESEND_PUBCOMP -mqtt_ms_wait_for_pubcomp = MessageState.MQTT_MS_WAIT_FOR_PUBCOMP -mqtt_ms_send_pubrec = MessageState.MQTT_MS_SEND_PUBREC -mqtt_ms_queued = MessageState.MQTT_MS_QUEUED - -MQTT_ERR_AGAIN = MQTTErrorCode.MQTT_ERR_AGAIN -MQTT_ERR_SUCCESS = MQTTErrorCode.MQTT_ERR_SUCCESS -MQTT_ERR_NOMEM = MQTTErrorCode.MQTT_ERR_NOMEM -MQTT_ERR_PROTOCOL = MQTTErrorCode.MQTT_ERR_PROTOCOL -MQTT_ERR_INVAL = MQTTErrorCode.MQTT_ERR_INVAL -MQTT_ERR_NO_CONN = MQTTErrorCode.MQTT_ERR_NO_CONN -MQTT_ERR_CONN_REFUSED = MQTTErrorCode.MQTT_ERR_CONN_REFUSED -MQTT_ERR_NOT_FOUND = MQTTErrorCode.MQTT_ERR_NOT_FOUND -MQTT_ERR_CONN_LOST = MQTTErrorCode.MQTT_ERR_CONN_LOST -MQTT_ERR_TLS = MQTTErrorCode.MQTT_ERR_TLS -MQTT_ERR_PAYLOAD_SIZE = MQTTErrorCode.MQTT_ERR_PAYLOAD_SIZE -MQTT_ERR_NOT_SUPPORTED = MQTTErrorCode.MQTT_ERR_NOT_SUPPORTED -MQTT_ERR_AUTH = MQTTErrorCode.MQTT_ERR_AUTH -MQTT_ERR_ACL_DENIED = MQTTErrorCode.MQTT_ERR_ACL_DENIED -MQTT_ERR_UNKNOWN = MQTTErrorCode.MQTT_ERR_UNKNOWN -MQTT_ERR_ERRNO = MQTTErrorCode.MQTT_ERR_ERRNO -MQTT_ERR_QUEUE_SIZE = MQTTErrorCode.MQTT_ERR_QUEUE_SIZE -MQTT_ERR_KEEPALIVE = MQTTErrorCode.MQTT_ERR_KEEPALIVE - -MQTTv31 = MQTTProtocolVersion.MQTTv31 -MQTTv311 = MQTTProtocolVersion.MQTTv311 -MQTTv5 = MQTTProtocolVersion.MQTTv5 - -MQTT_CLIENT = PahoClientMode.MQTT_CLIENT -MQTT_BRIDGE = PahoClientMode.MQTT_BRIDGE - -# For MQTT V5, use the clean start flag only on the first successful connect -MQTT_CLEAN_START_FIRST_ONLY: CleanStartOption = 3 - -sockpair_data = b"0" - -# Payload support all those type and will be converted to bytes: -# * str are utf8 encoded -# * int/float are converted to string and utf8 encoded (e.g. 1 is converted to b"1") -# * None is converted to a zero-length payload (i.e. b"") -PayloadType = Union[str, bytes, bytearray, int, float, None] - -HTTPHeader = Dict[str, str] -WebSocketHeaders = Union[Callable[[HTTPHeader], HTTPHeader], HTTPHeader] - -CleanStartOption = Union[bool, Literal[3]] - - -class ConnectFlags(NamedTuple): - """Contains additional information passed to `on_connect` callback""" - - session_present: bool - """ - this flag is useful for clients that are - using clean session set to False only (MQTTv3) or clean_start = False (MQTTv5). - In that case, if client that reconnects to a broker that it has previously - connected to, this flag indicates whether the broker still has the - session information for the client. If true, the session still exists. - """ - - -class DisconnectFlags(NamedTuple): - """Contains additional information passed to `on_disconnect` callback""" - - is_disconnect_packet_from_server: bool - """ - tells whether this on_disconnect call is the result - of receiving an DISCONNECT packet from the broker or if the on_disconnect is only - generated by the client library. - When true, the reason code is generated by the broker. - """ - - -CallbackOnConnect_v1_mqtt3 = Callable[["Client", Any, Dict[str, Any], MQTTErrorCode], None] -CallbackOnConnect_v1_mqtt5 = Callable[["Client", Any, Dict[str, Any], ReasonCode, Union[Properties, None]], None] -CallbackOnConnect_v1 = Union[CallbackOnConnect_v1_mqtt5, CallbackOnConnect_v1_mqtt3] -CallbackOnConnect_v2 = Callable[["Client", Any, ConnectFlags, ReasonCode, Union[Properties, None]], None] -CallbackOnConnect = Union[CallbackOnConnect_v1, CallbackOnConnect_v2] -CallbackOnConnectFail = Callable[["Client", Any], None] -CallbackOnDisconnect_v1_mqtt3 = Callable[["Client", Any, MQTTErrorCode], None] -CallbackOnDisconnect_v1_mqtt5 = Callable[["Client", Any, Union[ReasonCode, int, None], Union[Properties, None]], None] -CallbackOnDisconnect_v1 = Union[CallbackOnDisconnect_v1_mqtt3, CallbackOnDisconnect_v1_mqtt5] -CallbackOnDisconnect_v2 = Callable[["Client", Any, DisconnectFlags, ReasonCode, Union[Properties, None]], None] -CallbackOnDisconnect = Union[CallbackOnDisconnect_v1, CallbackOnDisconnect_v2] -CallbackOnLog = Callable[["Client", Any, int, str], None] -CallbackOnMessage = Callable[["Client", Any, "MQTTMessage"], None] -CallbackOnPreConnect = Callable[["Client", Any], None] -CallbackOnPublish_v1 = Callable[["Client", Any, int], None] -CallbackOnPublish_v2 = Callable[["Client", Any, int, ReasonCode, Properties], None] -CallbackOnPublish = Union[CallbackOnPublish_v1, CallbackOnPublish_v2] -CallbackOnSocket = Callable[["Client", Any, "SocketLike"], None] -CallbackOnSubscribe_v1_mqtt3 = Callable[["Client", Any, int, Tuple[int, ...]], None] -CallbackOnSubscribe_v1_mqtt5 = Callable[["Client", Any, int, List[ReasonCode], Properties], None] -CallbackOnSubscribe_v1 = Union[CallbackOnSubscribe_v1_mqtt3, CallbackOnSubscribe_v1_mqtt5] -CallbackOnSubscribe_v2 = Callable[["Client", Any, int, List[ReasonCode], Union[Properties, None]], None] -CallbackOnSubscribe = Union[CallbackOnSubscribe_v1, CallbackOnSubscribe_v2] -CallbackOnUnsubscribe_v1_mqtt3 = Callable[["Client", Any, int], None] -CallbackOnUnsubscribe_v1_mqtt5 = Callable[["Client", Any, int, Properties, Union[ReasonCode, List[ReasonCode]]], None] -CallbackOnUnsubscribe_v1 = Union[CallbackOnUnsubscribe_v1_mqtt3, CallbackOnUnsubscribe_v1_mqtt5] -CallbackOnUnsubscribe_v2 = Callable[["Client", Any, int, List[ReasonCode], Union[Properties, None]], None] -CallbackOnUnsubscribe = Union[CallbackOnUnsubscribe_v1, CallbackOnUnsubscribe_v2] - -# This is needed for typing because class Client redefined the name "socket" -_socket = socket - - -class WebsocketConnectionError(ConnectionError): - """ WebsocketConnectionError is a subclass of ConnectionError. - - It's raised when unable to perform the Websocket handshake. - """ - pass - - -def error_string(mqtt_errno: MQTTErrorCode | int) -> str: - """Return the error string associated with an mqtt error number.""" - if mqtt_errno == MQTT_ERR_SUCCESS: - return "No error." - elif mqtt_errno == MQTT_ERR_NOMEM: - return "Out of memory." - elif mqtt_errno == MQTT_ERR_PROTOCOL: - return "A network protocol error occurred when communicating with the broker." - elif mqtt_errno == MQTT_ERR_INVAL: - return "Invalid function arguments provided." - elif mqtt_errno == MQTT_ERR_NO_CONN: - return "The client is not currently connected." - elif mqtt_errno == MQTT_ERR_CONN_REFUSED: - return "The connection was refused." - elif mqtt_errno == MQTT_ERR_NOT_FOUND: - return "Message not found (internal error)." - elif mqtt_errno == MQTT_ERR_CONN_LOST: - return "The connection was lost." - elif mqtt_errno == MQTT_ERR_TLS: - return "A TLS error occurred." - elif mqtt_errno == MQTT_ERR_PAYLOAD_SIZE: - return "Payload too large." - elif mqtt_errno == MQTT_ERR_NOT_SUPPORTED: - return "This feature is not supported." - elif mqtt_errno == MQTT_ERR_AUTH: - return "Authorisation failed." - elif mqtt_errno == MQTT_ERR_ACL_DENIED: - return "Access denied by ACL." - elif mqtt_errno == MQTT_ERR_UNKNOWN: - return "Unknown error." - elif mqtt_errno == MQTT_ERR_ERRNO: - return "Error defined by errno." - elif mqtt_errno == MQTT_ERR_QUEUE_SIZE: - return "Message queue full." - elif mqtt_errno == MQTT_ERR_KEEPALIVE: - return "Client or broker did not communicate in the keepalive interval." - else: - return "Unknown error." - - -def connack_string(connack_code: int|ReasonCode) -> str: - """Return the string associated with a CONNACK result or CONNACK reason code.""" - if isinstance(connack_code, ReasonCode): - return str(connack_code) - - if connack_code == CONNACK_ACCEPTED: - return "Connection Accepted." - elif connack_code == CONNACK_REFUSED_PROTOCOL_VERSION: - return "Connection Refused: unacceptable protocol version." - elif connack_code == CONNACK_REFUSED_IDENTIFIER_REJECTED: - return "Connection Refused: identifier rejected." - elif connack_code == CONNACK_REFUSED_SERVER_UNAVAILABLE: - return "Connection Refused: broker unavailable." - elif connack_code == CONNACK_REFUSED_BAD_USERNAME_PASSWORD: - return "Connection Refused: bad user name or password." - elif connack_code == CONNACK_REFUSED_NOT_AUTHORIZED: - return "Connection Refused: not authorised." - else: - return "Connection Refused: unknown reason." - - -def convert_connack_rc_to_reason_code(connack_code: ConnackCode) -> ReasonCode: - """Convert a MQTTv3 / MQTTv3.1.1 connack result to `ReasonCode`. - - This is used in `on_connect` callback to have a consistent API. - - Be careful that the numeric value isn't the same, for example: - - >>> ConnackCode.CONNACK_REFUSED_SERVER_UNAVAILABLE == 3 - >>> convert_connack_rc_to_reason_code(ConnackCode.CONNACK_REFUSED_SERVER_UNAVAILABLE) == 136 - - It's recommended to compare by names - - >>> code_to_test = ReasonCode(PacketTypes.CONNACK, "Server unavailable") - >>> convert_connack_rc_to_reason_code(ConnackCode.CONNACK_REFUSED_SERVER_UNAVAILABLE) == code_to_test - """ - if connack_code == ConnackCode.CONNACK_ACCEPTED: - return ReasonCode(PacketTypes.CONNACK, "Success") - if connack_code == ConnackCode.CONNACK_REFUSED_PROTOCOL_VERSION: - return ReasonCode(PacketTypes.CONNACK, "Unsupported protocol version") - if connack_code == ConnackCode.CONNACK_REFUSED_IDENTIFIER_REJECTED: - return ReasonCode(PacketTypes.CONNACK, "Client identifier not valid") - if connack_code == ConnackCode.CONNACK_REFUSED_SERVER_UNAVAILABLE: - return ReasonCode(PacketTypes.CONNACK, "Server unavailable") - if connack_code == ConnackCode.CONNACK_REFUSED_BAD_USERNAME_PASSWORD: - return ReasonCode(PacketTypes.CONNACK, "Bad user name or password") - if connack_code == ConnackCode.CONNACK_REFUSED_NOT_AUTHORIZED: - return ReasonCode(PacketTypes.CONNACK, "Not authorized") - - return ReasonCode(PacketTypes.CONNACK, "Unspecified error") - - -def convert_disconnect_error_code_to_reason_code(rc: MQTTErrorCode) -> ReasonCode: - """Convert an MQTTErrorCode to Reason code. - - This is used in `on_disconnect` callback to have a consistent API. - - Be careful that the numeric value isn't the same, for example: - - >>> MQTTErrorCode.MQTT_ERR_PROTOCOL == 2 - >>> convert_disconnect_error_code_to_reason_code(MQTTErrorCode.MQTT_ERR_PROTOCOL) == 130 - - It's recommended to compare by names - - >>> code_to_test = ReasonCode(PacketTypes.DISCONNECT, "Protocol error") - >>> convert_disconnect_error_code_to_reason_code(MQTTErrorCode.MQTT_ERR_PROTOCOL) == code_to_test - """ - if rc == MQTTErrorCode.MQTT_ERR_SUCCESS: - return ReasonCode(PacketTypes.DISCONNECT, "Success") - if rc == MQTTErrorCode.MQTT_ERR_KEEPALIVE: - return ReasonCode(PacketTypes.DISCONNECT, "Keep alive timeout") - if rc == MQTTErrorCode.MQTT_ERR_CONN_LOST: - return ReasonCode(PacketTypes.DISCONNECT, "Unspecified error") - return ReasonCode(PacketTypes.DISCONNECT, "Unspecified error") - - -def _base62( - num: int, - base: str = string.digits + string.ascii_letters, - padding: int = 1, -) -> str: - """Convert a number to base-62 representation.""" - if num < 0: - raise ValueError("Number must be positive or zero") - digits = [] - while num: - num, rest = divmod(num, 62) - digits.append(base[rest]) - digits.extend(base[0] for _ in range(len(digits), padding)) - return ''.join(reversed(digits)) - - -def topic_matches_sub(sub: str, topic: str) -> bool: - """Check whether a topic matches a subscription. - - For example: - - * Topic "foo/bar" would match the subscription "foo/#" or "+/bar" - * Topic "non/matching" would not match the subscription "non/+/+" - """ - matcher = MQTTMatcher() - matcher[sub] = True - try: - next(matcher.iter_match(topic)) - return True - except StopIteration: - return False - - -def _socketpair_compat() -> tuple[socket.socket, socket.socket]: - """TCP/IP socketpair including Windows support""" - listensock = socket.socket( - socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_IP) - listensock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - listensock.bind(("127.0.0.1", 0)) - listensock.listen(1) - - iface, port = listensock.getsockname() - sock1 = socket.socket( - socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_IP) - sock1.setblocking(False) - try: - sock1.connect(("127.0.0.1", port)) - except BlockingIOError: - pass - sock2, address = listensock.accept() - sock2.setblocking(False) - listensock.close() - return (sock1, sock2) - - -def _force_bytes(s: str | bytes) -> bytes: - if isinstance(s, str): - return s.encode("utf-8") - return s - - -def _encode_payload(payload: str | bytes | bytearray | int | float | None) -> bytes|bytearray: - if isinstance(payload, str): - return payload.encode("utf-8") - - if isinstance(payload, (int, float)): - return str(payload).encode("ascii") - - if payload is None: - return b"" - - if not isinstance(payload, (bytes, bytearray)): - raise TypeError( - "payload must be a string, bytearray, int, float or None." - ) - - return payload - - -class MQTTMessageInfo: - """This is a class returned from `Client.publish()` and can be used to find - out the mid of the message that was published, and to determine whether the - message has been published, and/or wait until it is published. - """ - - __slots__ = 'mid', '_published', '_condition', 'rc', '_iterpos' - - def __init__(self, mid: int): - self.mid = mid - """ The message Id (int)""" - self._published = False - self._condition = threading.Condition() - self.rc: MQTTErrorCode = MQTTErrorCode.MQTT_ERR_SUCCESS - """ The `MQTTErrorCode` that give status for this message. - This value could change until the message `is_published`""" - self._iterpos = 0 - - def __str__(self) -> str: - return str((self.rc, self.mid)) - - def __iter__(self) -> Iterator[MQTTErrorCode | int]: - self._iterpos = 0 - return self - - def __next__(self) -> MQTTErrorCode | int: - return self.next() - - def next(self) -> MQTTErrorCode | int: - if self._iterpos == 0: - self._iterpos = 1 - return self.rc - elif self._iterpos == 1: - self._iterpos = 2 - return self.mid - else: - raise StopIteration - - def __getitem__(self, index: int) -> MQTTErrorCode | int: - if index == 0: - return self.rc - elif index == 1: - return self.mid - else: - raise IndexError("index out of range") - - def _set_as_published(self) -> None: - with self._condition: - self._published = True - self._condition.notify() - - def wait_for_publish(self, timeout: float | None = None) -> None: - """Block until the message associated with this object is published, or - until the timeout occurs. If timeout is None, this will never time out. - Set timeout to a positive number of seconds, e.g. 1.2, to enable the - timeout. - - :raises ValueError: if the message was not queued due to the outgoing - queue being full. - - :raises RuntimeError: if the message was not published for another - reason. - """ - if self.rc == MQTT_ERR_QUEUE_SIZE: - raise ValueError('Message is not queued due to ERR_QUEUE_SIZE') - elif self.rc == MQTT_ERR_AGAIN: - pass - elif self.rc > 0: - raise RuntimeError(f'Message publish failed: {error_string(self.rc)}') - - timeout_time = None if timeout is None else time_func() + timeout - timeout_tenth = None if timeout is None else timeout / 10. - def timed_out() -> bool: - return False if timeout_time is None else time_func() > timeout_time - - with self._condition: - while not self._published and not timed_out(): - self._condition.wait(timeout_tenth) - - if self.rc > 0: - raise RuntimeError(f'Message publish failed: {error_string(self.rc)}') - - def is_published(self) -> bool: - """Returns True if the message associated with this object has been - published, else returns False. - - To wait for this to become true, look at `wait_for_publish`. - """ - if self.rc == MQTTErrorCode.MQTT_ERR_QUEUE_SIZE: - raise ValueError('Message is not queued due to ERR_QUEUE_SIZE') - elif self.rc == MQTTErrorCode.MQTT_ERR_AGAIN: - pass - elif self.rc > 0: - raise RuntimeError(f'Message publish failed: {error_string(self.rc)}') - - with self._condition: - return self._published - - -class MQTTMessage: - """ This is a class that describes an incoming message. It is - passed to the `on_message` callback as the message parameter. - """ - __slots__ = 'timestamp', 'state', 'dup', 'mid', '_topic', 'payload', 'qos', 'retain', 'info', 'properties' - - def __init__(self, mid: int = 0, topic: bytes = b""): - self.timestamp = 0.0 - self.state = mqtt_ms_invalid - self.dup = False - self.mid = mid - """ The message id (int).""" - self._topic = topic - self.payload = b"" - """the message payload (bytes)""" - self.qos = 0 - """ The message Quality of Service (0, 1 or 2).""" - self.retain = False - """ If true, the message is a retained message and not fresh.""" - self.info = MQTTMessageInfo(mid) - self.properties: Properties | None = None - """ In MQTT v5.0, the properties associated with the message. (`Properties`)""" - - def __eq__(self, other: object) -> bool: - """Override the default Equals behavior""" - if isinstance(other, self.__class__): - return self.mid == other.mid - return False - - def __ne__(self, other: object) -> bool: - """Define a non-equality test""" - return not self.__eq__(other) - - @property - def topic(self) -> str: - """topic that the message was published on. - - This property is read-only. - """ - return self._topic.decode('utf-8') - - @topic.setter - def topic(self, value: bytes) -> None: - self._topic = value - - -class Client: - """MQTT version 3.1/3.1.1/5.0 client class. - - This is the main class for use communicating with an MQTT broker. - - General usage flow: - - * Use `connect()`, `connect_async()` or `connect_srv()` to connect to a broker - * Use `loop_start()` to set a thread running to call `loop()` for you. - * Or use `loop_forever()` to handle calling `loop()` for you in a blocking function. - * Or call `loop()` frequently to maintain network traffic flow with the broker - * Use `subscribe()` to subscribe to a topic and receive messages - * Use `publish()` to send messages - * Use `disconnect()` to disconnect from the broker - - Data returned from the broker is made available with the use of callback - functions as described below. - - :param CallbackAPIVersion callback_api_version: define the API version for user-callback (on_connect, on_publish,...). - This field is required and it's recommended to use the latest version (CallbackAPIVersion.API_VERSION2). - See each callback for description of API for each version. The file docs/migrations.rst contains details on - how to migrate between version. - - :param str client_id: the unique client id string used when connecting to the - broker. If client_id is zero length or None, then the behaviour is - defined by which protocol version is in use. If using MQTT v3.1.1, then - a zero length client id will be sent to the broker and the broker will - generate a random for the client. If using MQTT v3.1 then an id will be - randomly generated. In both cases, clean_session must be True. If this - is not the case a ValueError will be raised. - - :param bool clean_session: a boolean that determines the client type. If True, - the broker will remove all information about this client when it - disconnects. If False, the client is a persistent client and - subscription information and queued messages will be retained when the - client disconnects. - Note that a client will never discard its own outgoing messages on - disconnect. Calling connect() or reconnect() will cause the messages to - be resent. Use reinitialise() to reset a client to its original state. - The clean_session argument only applies to MQTT versions v3.1.1 and v3.1. - It is not accepted if the MQTT version is v5.0 - use the clean_start - argument on connect() instead. - - :param userdata: user defined data of any type that is passed as the "userdata" - parameter to callbacks. It may be updated at a later point with the - user_data_set() function. - - :param int protocol: allows explicit setting of the MQTT version to - use for this client. Can be paho.mqtt.client.MQTTv311 (v3.1.1), - paho.mqtt.client.MQTTv31 (v3.1) or paho.mqtt.client.MQTTv5 (v5.0), - with the default being v3.1.1. - - :param transport: use "websockets" to use WebSockets as the transport - mechanism. Set to "tcp" to use raw TCP, which is the default. - Use "unix" to use Unix sockets as the transport mechanism; note that - this option is only available on platforms that support Unix sockets, - and the "host" argument is interpreted as the path to the Unix socket - file in this case. - - :param bool manual_ack: normally, when a message is received, the library automatically - acknowledges after on_message callback returns. manual_ack=True allows the application to - acknowledge receipt after it has completed processing of a message - using a the ack() method. This addresses vulnerability to message loss - if applications fails while processing a message, or while it pending - locally. - - Callbacks - ========= - - A number of callback functions are available to receive data back from the - broker. To use a callback, define a function and then assign it to the - client:: - - def on_connect(client, userdata, flags, reason_code, properties): - print(f"Connected with result code {reason_code}") - - client.on_connect = on_connect - - Callbacks can also be attached using decorators:: - - mqttc = paho.mqtt.Client() - - @mqttc.connect_callback() - def on_connect(client, userdata, flags, reason_code, properties): - print(f"Connected with result code {reason_code}") - - All of the callbacks as described below have a "client" and an "userdata" - argument. "client" is the `Client` instance that is calling the callback. - userdata" is user data of any type and can be set when creating a new client - instance or with `user_data_set()`. - - If you wish to suppress exceptions within a callback, you should set - ``mqttc.suppress_exceptions = True`` - - The callbacks are listed below, documentation for each of them can be found - at the same function name: - - `on_connect`, `on_connect_fail`, `on_disconnect`, `on_message`, `on_publish`, - `on_subscribe`, `on_unsubscribe`, `on_log`, `on_socket_open`, `on_socket_close`, - `on_socket_register_write`, `on_socket_unregister_write` - """ - - def __init__( - self, - callback_api_version: CallbackAPIVersion = CallbackAPIVersion.VERSION1, - client_id: str | None = "", - clean_session: bool | None = None, - userdata: Any = None, - protocol: MQTTProtocolVersion = MQTTv311, - transport: Literal["tcp", "websockets", "unix"] = "tcp", - reconnect_on_failure: bool = True, - manual_ack: bool = False, - ) -> None: - transport = transport.lower() # type: ignore - if transport == "unix" and not hasattr(socket, "AF_UNIX"): - raise ValueError('"unix" transport not supported') - elif transport not in ("websockets", "tcp", "unix"): - raise ValueError( - f'transport must be "websockets", "tcp" or "unix", not {transport}') - - self._manual_ack = manual_ack - self._transport = transport - self._protocol = protocol - self._userdata = userdata - self._sock: SocketLike | None = None - self._sockpairR: socket.socket | None = None - self._sockpairW: socket.socket | None = None - self._keepalive = 60 - self._connect_timeout = 5.0 - self._client_mode = MQTT_CLIENT - self._callback_api_version = callback_api_version - - if self._callback_api_version == CallbackAPIVersion.VERSION1: - warnings.warn( - "Callback API version 1 is deprecated, update to latest version", - category=DeprecationWarning, - stacklevel=2, - ) - if isinstance(self._callback_api_version, str): - # Help user to migrate, it probably provided a client id - # as first arguments - raise ValueError( - "Unsupported callback API version: version 2.0 added a callback_api_version, see docs/migrations.rst for details" - ) - if self._callback_api_version not in CallbackAPIVersion: - raise ValueError("Unsupported callback API version") - - self._clean_start: int = MQTT_CLEAN_START_FIRST_ONLY - - if protocol == MQTTv5: - if clean_session is not None: - raise ValueError('Clean session is not used for MQTT 5.0') - else: - if clean_session is None: - clean_session = True - if not clean_session and (client_id == "" or client_id is None): - raise ValueError( - 'A client id must be provided if clean session is False.') - self._clean_session = clean_session - - # [MQTT-3.1.3-4] Client Id must be UTF-8 encoded string. - if client_id == "" or client_id is None: - if protocol == MQTTv31: - self._client_id = _base62(uuid.uuid4().int, padding=22).encode("utf8") - else: - self._client_id = b"" - else: - self._client_id = _force_bytes(client_id) - - self._username: bytes | None = None - self._password: bytes | None = None - self._in_packet: _InPacket = { - "command": 0, - "have_remaining": 0, - "remaining_count": [], - "remaining_mult": 1, - "remaining_length": 0, - "packet": bytearray(b""), - "to_process": 0, - "pos": 0, - } - self._out_packet: collections.deque[_OutPacket] = collections.deque() - self._last_msg_in = time_func() - self._last_msg_out = time_func() - self._reconnect_min_delay = 1 - self._reconnect_max_delay = 120 - self._reconnect_delay: int | None = None - self._reconnect_on_failure = reconnect_on_failure - self._ping_t = 0.0 - self._last_mid = 0 - self._state = _ConnectionState.MQTT_CS_NEW - self._out_messages: collections.OrderedDict[ - int, MQTTMessage - ] = collections.OrderedDict() - self._in_messages: collections.OrderedDict[ - int, MQTTMessage - ] = collections.OrderedDict() - self._max_inflight_messages = 20 - self._inflight_messages = 0 - self._max_queued_messages = 0 - self._connect_properties: Properties | None = None - self._will_properties: Properties | None = None - self._will = False - self._will_topic = b"" - self._will_payload = b"" - self._will_qos = 0 - self._will_retain = False - self._on_message_filtered = MQTTMatcher() - self._host = "" - self._port = 1883 - self._bind_address = "" - self._bind_port = 0 - self._proxy: Any = {} - self._in_callback_mutex = threading.Lock() - self._callback_mutex = threading.RLock() - self._msgtime_mutex = threading.Lock() - self._out_message_mutex = threading.RLock() - self._in_message_mutex = threading.Lock() - self._reconnect_delay_mutex = threading.Lock() - self._mid_generate_mutex = threading.Lock() - self._thread: threading.Thread | None = None - self._thread_terminate = False - self._ssl = False - self._ssl_context: ssl.SSLContext | None = None - # Only used when SSL context does not have check_hostname attribute - self._tls_insecure = False - self._logger: logging.Logger | None = None - self._registered_write = False - # No default callbacks - self._on_log: CallbackOnLog | None = None - self._on_pre_connect: CallbackOnPreConnect | None = None - self._on_connect: CallbackOnConnect | None = None - self._on_connect_fail: CallbackOnConnectFail | None = None - self._on_subscribe: CallbackOnSubscribe | None = None - self._on_message: CallbackOnMessage | None = None - self._on_publish: CallbackOnPublish | None = None - self._on_unsubscribe: CallbackOnUnsubscribe | None = None - self._on_disconnect: CallbackOnDisconnect | None = None - self._on_socket_open: CallbackOnSocket | None = None - self._on_socket_close: CallbackOnSocket | None = None - self._on_socket_register_write: CallbackOnSocket | None = None - self._on_socket_unregister_write: CallbackOnSocket | None = None - self._websocket_path = "/mqtt" - self._websocket_extra_headers: WebSocketHeaders | None = None - # for clean_start == MQTT_CLEAN_START_FIRST_ONLY - self._mqttv5_first_connect = True - self.suppress_exceptions = False # For callbacks - - def __del__(self) -> None: - self._reset_sockets() - - @property - def host(self) -> str: - """ - Host to connect to. If `connect()` hasn't been called yet, returns an empty string. - - This property may not be changed if the connection is already open. - """ - return self._host - - @host.setter - def host(self, value: str) -> None: - if not self._connection_closed(): - raise RuntimeError("updating host on established connection is not supported") - - if not value: - raise ValueError("Invalid host.") - self._host = value - - @property - def port(self) -> int: - """ - Broker TCP port to connect to. - - This property may not be changed if the connection is already open. - """ - return self._port - - @port.setter - def port(self, value: int) -> None: - if not self._connection_closed(): - raise RuntimeError("updating port on established connection is not supported") - - if value <= 0: - raise ValueError("Invalid port number.") - self._port = value - - @property - def keepalive(self) -> int: - """ - Client keepalive interval (in seconds). - - This property may not be changed if the connection is already open. - """ - return self._keepalive - - @keepalive.setter - def keepalive(self, value: int) -> None: - if not self._connection_closed(): - # The issue here is that the previous value of keepalive matter to possibly - # sent ping packet. - raise RuntimeError("updating keepalive on established connection is not supported") - - if value < 0: - raise ValueError("Keepalive must be >=0.") - - self._keepalive = value - - @property - def transport(self) -> Literal["tcp", "websockets", "unix"]: - """ - Transport method used for the connection ("tcp" or "websockets"). - - This property may not be changed if the connection is already open. - """ - return self._transport - - @transport.setter - def transport(self, value: Literal["tcp", "websockets"]) -> None: - if not self._connection_closed(): - raise RuntimeError("updating transport on established connection is not supported") - - self._transport = value - - @property - def protocol(self) -> MQTTProtocolVersion: - """ - Protocol version used (MQTT v3, MQTT v3.11, MQTTv5) - - This property is read-only. - """ - return self._protocol - - @property - def connect_timeout(self) -> float: - """ - Connection establishment timeout in seconds. - - This property may not be changed if the connection is already open. - """ - return self._connect_timeout - - @connect_timeout.setter - def connect_timeout(self, value: float) -> None: - if not self._connection_closed(): - raise RuntimeError("updating connect_timeout on established connection is not supported") - - if value <= 0.0: - raise ValueError("timeout must be a positive number") - - self._connect_timeout = value - - @property - def username(self) -> str | None: - """The username used to connect to the MQTT broker, or None if no username is used. - - This property may not be changed if the connection is already open. - """ - if self._username is None: - return None - return self._username.decode("utf-8") - - @username.setter - def username(self, value: str | None) -> None: - if not self._connection_closed(): - raise RuntimeError("updating username on established connection is not supported") - - if value is None: - self._username = None - else: - self._username = value.encode("utf-8") - - @property - def password(self) -> str | None: - """The password used to connect to the MQTT broker, or None if no password is used. - - This property may not be changed if the connection is already open. - """ - if self._password is None: - return None - return self._password.decode("utf-8") - - @password.setter - def password(self, value: str | None) -> None: - if not self._connection_closed(): - raise RuntimeError("updating password on established connection is not supported") - - if value is None: - self._password = None - else: - self._password = value.encode("utf-8") - - @property - def max_inflight_messages(self) -> int: - """ - Maximum number of messages with QoS > 0 that can be partway through the network flow at once - - This property may not be changed if the connection is already open. - """ - return self._max_inflight_messages - - @max_inflight_messages.setter - def max_inflight_messages(self, value: int) -> None: - if not self._connection_closed(): - # Not tested. Some doubt that everything is okay when max_inflight change between 0 - # and > 0 value because _update_inflight is skipped when _max_inflight_messages == 0 - raise RuntimeError("updating max_inflight_messages on established connection is not supported") - - if value < 0: - raise ValueError("Invalid inflight.") - - self._max_inflight_messages = value - - @property - def max_queued_messages(self) -> int: - """ - Maximum number of message in the outgoing message queue, 0 means unlimited - - This property may not be changed if the connection is already open. - """ - return self._max_queued_messages - - @max_queued_messages.setter - def max_queued_messages(self, value: int) -> None: - if not self._connection_closed(): - # Not tested. - raise RuntimeError("updating max_queued_messages on established connection is not supported") - - if value < 0: - raise ValueError("Invalid queue size.") - - self._max_queued_messages = value - - @property - def will_topic(self) -> str | None: - """ - The topic name a will message is sent to when disconnecting unexpectedly. None if a will shall not be sent. - - This property is read-only. Use `will_set()` to change its value. - """ - if self._will_topic is None: - return None - - return self._will_topic.decode("utf-8") - - @property - def will_payload(self) -> bytes | None: - """ - The payload for the will message that is sent when disconnecting unexpectedly. None if a will shall not be sent. - - This property is read-only. Use `will_set()` to change its value. - """ - return self._will_payload - - @property - def logger(self) -> logging.Logger | None: - return self._logger - - @logger.setter - def logger(self, value: logging.Logger | None) -> None: - self._logger = value - - def _sock_recv(self, bufsize: int) -> bytes: - if self._sock is None: - raise ConnectionError("self._sock is None") - try: - return self._sock.recv(bufsize) - except ssl.SSLWantReadError as err: - raise BlockingIOError() from err - except ssl.SSLWantWriteError as err: - self._call_socket_register_write() - raise BlockingIOError() from err - except AttributeError as err: - self._easy_log( - MQTT_LOG_DEBUG, "socket was None: %s", err) - raise ConnectionError() from err - - def _sock_send(self, buf: bytes) -> int: - if self._sock is None: - raise ConnectionError("self._sock is None") - - try: - return self._sock.send(buf) - except ssl.SSLWantReadError as err: - raise BlockingIOError() from err - except ssl.SSLWantWriteError as err: - self._call_socket_register_write() - raise BlockingIOError() from err - except BlockingIOError as err: - self._call_socket_register_write() - raise BlockingIOError() from err - - def _sock_close(self) -> None: - """Close the connection to the server.""" - if not self._sock: - return - - try: - sock = self._sock - self._sock = None - self._call_socket_unregister_write(sock) - self._call_socket_close(sock) - finally: - # In case a callback fails, still close the socket to avoid leaking the file descriptor. - sock.close() - - def _reset_sockets(self, sockpair_only: bool = False) -> None: - if not sockpair_only: - self._sock_close() - - if self._sockpairR: - self._sockpairR.close() - self._sockpairR = None - if self._sockpairW: - self._sockpairW.close() - self._sockpairW = None - - def reinitialise( - self, - client_id: str = "", - clean_session: bool = True, - userdata: Any = None, - ) -> None: - self._reset_sockets() - - self.__init__(client_id, clean_session, userdata) # type: ignore[misc] - - def ws_set_options( - self, - path: str = "/mqtt", - headers: WebSocketHeaders | None = None, - ) -> None: - """ Set the path and headers for a websocket connection - - :param str path: a string starting with / which should be the endpoint of the - mqtt connection on the remote server - - :param headers: can be either a dict or a callable object. If it is a dict then - the extra items in the dict are added to the websocket headers. If it is - a callable, then the default websocket headers are passed into this - function and the result is used as the new headers. - """ - self._websocket_path = path - - if headers is not None: - if isinstance(headers, dict) or callable(headers): - self._websocket_extra_headers = headers - else: - raise ValueError( - "'headers' option to ws_set_options has to be either a dictionary or callable") - - def tls_set_context( - self, - context: ssl.SSLContext | None = None, - ) -> None: - """Configure network encryption and authentication context. Enables SSL/TLS support. - - :param context: an ssl.SSLContext object. By default this is given by - ``ssl.create_default_context()``, if available. - - Must be called before `connect()`, `connect_async()` or `connect_srv()`.""" - if self._ssl_context is not None: - raise ValueError('SSL/TLS has already been configured.') - - if context is None: - context = ssl.create_default_context() - - self._ssl = True - self._ssl_context = context - - # Ensure _tls_insecure is consistent with check_hostname attribute - if hasattr(context, 'check_hostname'): - self._tls_insecure = not context.check_hostname - - def tls_set( - self, - ca_certs: str | None = None, - certfile: str | None = None, - keyfile: str | None = None, - cert_reqs: ssl.VerifyMode | None = None, - tls_version: int | None = None, - ciphers: str | None = None, - keyfile_password: str | None = None, - alpn_protocols: list[str] | None = None, - ) -> None: - """Configure network encryption and authentication options. Enables SSL/TLS support. - - :param str ca_certs: a string path to the Certificate Authority certificate files - that are to be treated as trusted by this client. If this is the only - option given then the client will operate in a similar manner to a web - browser. That is to say it will require the broker to have a - certificate signed by the Certificate Authorities in ca_certs and will - communicate using TLS v1,2, but will not attempt any form of - authentication. This provides basic network encryption but may not be - sufficient depending on how the broker is configured. - - By default, on Python 2.7.9+ or 3.4+, the default certification - authority of the system is used. On older Python version this parameter - is mandatory. - :param str certfile: PEM encoded client certificate filename. Used with - keyfile for client TLS based authentication. Support for this feature is - broker dependent. Note that if the files in encrypted and needs a password to - decrypt it, then this can be passed using the keyfile_password argument - you - should take precautions to ensure that your password is - not hard coded into your program by loading the password from a file - for example. If you do not provide keyfile_password, the password will - be requested to be typed in at a terminal window. - :param str keyfile: PEM encoded client private keys filename. Used with - certfile for client TLS based authentication. Support for this feature is - broker dependent. Note that if the files in encrypted and needs a password to - decrypt it, then this can be passed using the keyfile_password argument - you - should take precautions to ensure that your password is - not hard coded into your program by loading the password from a file - for example. If you do not provide keyfile_password, the password will - be requested to be typed in at a terminal window. - :param cert_reqs: the certificate requirements that the client imposes - on the broker to be changed. By default this is ssl.CERT_REQUIRED, - which means that the broker must provide a certificate. See the ssl - pydoc for more information on this parameter. - :param tls_version: the version of the SSL/TLS protocol used to be - specified. By default TLS v1.2 is used. Previous versions are allowed - but not recommended due to possible security problems. - :param str ciphers: encryption ciphers that are allowed - for this connection, or None to use the defaults. See the ssl pydoc for - more information. - - Must be called before `connect()`, `connect_async()` or `connect_srv()`.""" - if ssl is None: - raise ValueError('This platform has no SSL/TLS.') - - if not hasattr(ssl, 'SSLContext'): - # Require Python version that has SSL context support in standard library - raise ValueError( - 'Python 2.7.9 and 3.2 are the minimum supported versions for TLS.') - - if ca_certs is None and not hasattr(ssl.SSLContext, 'load_default_certs'): - raise ValueError('ca_certs must not be None.') - - # Create SSLContext object - if tls_version is None: - tls_version = ssl.PROTOCOL_TLSv1_2 - # If the python version supports it, use highest TLS version automatically - if hasattr(ssl, "PROTOCOL_TLS_CLIENT"): - # This also enables CERT_REQUIRED and check_hostname by default. - tls_version = ssl.PROTOCOL_TLS_CLIENT - elif hasattr(ssl, "PROTOCOL_TLS"): - tls_version = ssl.PROTOCOL_TLS - context = ssl.SSLContext(tls_version) - - # Configure context - if ciphers is not None: - context.set_ciphers(ciphers) - - if certfile is not None: - context.load_cert_chain(certfile, keyfile, keyfile_password) - - if cert_reqs == ssl.CERT_NONE and hasattr(context, 'check_hostname'): - context.check_hostname = False - - context.verify_mode = ssl.CERT_REQUIRED if cert_reqs is None else cert_reqs - - if ca_certs is not None: - context.load_verify_locations(ca_certs) - else: - context.load_default_certs() - - if alpn_protocols is not None: - if not getattr(ssl, "HAS_ALPN", None): - raise ValueError("SSL library has no support for ALPN") - context.set_alpn_protocols(alpn_protocols) - - self.tls_set_context(context) - - if cert_reqs != ssl.CERT_NONE: - # Default to secure, sets context.check_hostname attribute - # if available - self.tls_insecure_set(False) - else: - # But with ssl.CERT_NONE, we can not check_hostname - self.tls_insecure_set(True) - - def tls_insecure_set(self, value: bool) -> None: - """Configure verification of the server hostname in the server certificate. - - If value is set to true, it is impossible to guarantee that the host - you are connecting to is not impersonating your server. This can be - useful in initial server testing, but makes it possible for a malicious - third party to impersonate your server through DNS spoofing, for - example. - - Do not use this function in a real system. Setting value to true means - there is no point using encryption. - - Must be called before `connect()` and after either `tls_set()` or - `tls_set_context()`.""" - - if self._ssl_context is None: - raise ValueError( - 'Must configure SSL context before using tls_insecure_set.') - - self._tls_insecure = value - - # Ensure check_hostname is consistent with _tls_insecure attribute - if hasattr(self._ssl_context, 'check_hostname'): - # Rely on SSLContext to check host name - # If verify_mode is CERT_NONE then the host name will never be checked - self._ssl_context.check_hostname = not value - - def proxy_set(self, **proxy_args: Any) -> None: - """Configure proxying of MQTT connection. Enables support for SOCKS or - HTTP proxies. - - Proxying is done through the PySocks library. Brief descriptions of the - proxy_args parameters are below; see the PySocks docs for more info. - - (Required) - - :param proxy_type: One of {socks.HTTP, socks.SOCKS4, or socks.SOCKS5} - :param proxy_addr: IP address or DNS name of proxy server - - (Optional) - - :param proxy_port: (int) port number of the proxy server. If not provided, - the PySocks package default value will be utilized, which differs by proxy_type. - :param proxy_rdns: boolean indicating whether proxy lookup should be performed - remotely (True, default) or locally (False) - :param proxy_username: username for SOCKS5 proxy, or userid for SOCKS4 proxy - :param proxy_password: password for SOCKS5 proxy - - Example:: - - mqttc.proxy_set(proxy_type=socks.HTTP, proxy_addr='1.2.3.4', proxy_port=4231) - """ - if socks is None: - raise ValueError("PySocks must be installed for proxy support.") - elif not self._proxy_is_valid(proxy_args): - raise ValueError("proxy_type and/or proxy_addr are invalid.") - else: - self._proxy = proxy_args - - def enable_logger(self, logger: logging.Logger | None = None) -> None: - """ - Enables a logger to send log messages to - - :param logging.Logger logger: if specified, that ``logging.Logger`` object will be used, otherwise - one will be created automatically. - - See `disable_logger` to undo this action. - """ - if logger is None: - if self._logger is not None: - # Do not replace existing logger - return - logger = logging.getLogger(__name__) - self.logger = logger - - def disable_logger(self) -> None: - """ - Disable logging using standard python logging package. This has no effect on the `on_log` callback. - """ - self._logger = None - - def connect( - self, - host: str, - port: int = 1883, - keepalive: int = 60, - bind_address: str = "", - bind_port: int = 0, - clean_start: CleanStartOption = MQTT_CLEAN_START_FIRST_ONLY, - properties: Properties | None = None, - ) -> MQTTErrorCode: - """Connect to a remote broker. This is a blocking call that establishes - the underlying connection and transmits a CONNECT packet. - Note that the connection status will not be updated until a CONNACK is received and - processed (this requires a running network loop, see `loop_start`, `loop_forever`, `loop`...). - - :param str host: the hostname or IP address of the remote broker. - :param int port: the network port of the server host to connect to. Defaults to - 1883. Note that the default port for MQTT over SSL/TLS is 8883 so if you - are using `tls_set()` the port may need providing. - :param int keepalive: Maximum period in seconds between communications with the - broker. If no other messages are being exchanged, this controls the - rate at which the client will send ping messages to the broker. - :param bool clean_start: (MQTT v5.0 only) True, False or MQTT_CLEAN_START_FIRST_ONLY. - Sets the MQTT v5.0 clean_start flag always, never or on the first successful connect only, - respectively. MQTT session data (such as outstanding messages and subscriptions) - is cleared on successful connect when the clean_start flag is set. - For MQTT v3.1.1, the ``clean_session`` argument of `Client` should be used for similar - result. - :param Properties properties: (MQTT v5.0 only) the MQTT v5.0 properties to be sent in the - MQTT connect packet. - """ - - if self._protocol == MQTTv5: - self._mqttv5_first_connect = True - else: - if clean_start != MQTT_CLEAN_START_FIRST_ONLY: - raise ValueError("Clean start only applies to MQTT V5") - if properties: - raise ValueError("Properties only apply to MQTT V5") - - self.connect_async(host, port, keepalive, - bind_address, bind_port, clean_start, properties) - return self.reconnect() - - def connect_srv( - self, - domain: str | None = None, - keepalive: int = 60, - bind_address: str = "", - bind_port: int = 0, - clean_start: CleanStartOption = MQTT_CLEAN_START_FIRST_ONLY, - properties: Properties | None = None, - ) -> MQTTErrorCode: - """Connect to a remote broker. - - :param str domain: the DNS domain to search for SRV records; if None, - try to determine local domain name. - :param keepalive, bind_address, clean_start and properties: see `connect()` - """ - - if HAVE_DNS is False: - raise ValueError( - 'No DNS resolver library found, try "pip install dnspython".') - - if domain is None: - domain = socket.getfqdn() - domain = domain[domain.find('.') + 1:] - - try: - rr = f'_mqtt._tcp.{domain}' - if self._ssl: - # IANA specifies secure-mqtt (not mqtts) for port 8883 - rr = f'_secure-mqtt._tcp.{domain}' - answers = [] - for answer in dns.resolver.query(rr, dns.rdatatype.SRV): - addr = answer.target.to_text()[:-1] - answers.append( - (addr, answer.port, answer.priority, answer.weight)) - except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers) as err: - raise ValueError(f"No answer/NXDOMAIN for SRV in {domain}") from err - - # FIXME: doesn't account for weight - for answer in answers: - host, port, prio, weight = answer - - try: - return self.connect(host, port, keepalive, bind_address, bind_port, clean_start, properties) - except Exception: # noqa: S110 - pass - - raise ValueError("No SRV hosts responded") - - def connect_async( - self, - host: str, - port: int = 1883, - keepalive: int = 60, - bind_address: str = "", - bind_port: int = 0, - clean_start: CleanStartOption = MQTT_CLEAN_START_FIRST_ONLY, - properties: Properties | None = None, - ) -> None: - """Connect to a remote broker asynchronously. This is a non-blocking - connect call that can be used with `loop_start()` to provide very quick - start. - - Any already established connection will be terminated immediately. - - :param str host: the hostname or IP address of the remote broker. - :param int port: the network port of the server host to connect to. Defaults to - 1883. Note that the default port for MQTT over SSL/TLS is 8883 so if you - are using `tls_set()` the port may need providing. - :param int keepalive: Maximum period in seconds between communications with the - broker. If no other messages are being exchanged, this controls the - rate at which the client will send ping messages to the broker. - :param bool clean_start: (MQTT v5.0 only) True, False or MQTT_CLEAN_START_FIRST_ONLY. - Sets the MQTT v5.0 clean_start flag always, never or on the first successful connect only, - respectively. MQTT session data (such as outstanding messages and subscriptions) - is cleared on successful connect when the clean_start flag is set. - For MQTT v3.1.1, the ``clean_session`` argument of `Client` should be used for similar - result. - :param Properties properties: (MQTT v5.0 only) the MQTT v5.0 properties to be sent in the - MQTT connect packet. - """ - if bind_port < 0: - raise ValueError('Invalid bind port number.') - - # Switch to state NEW to allow update of host, port & co. - self._sock_close() - self._state = _ConnectionState.MQTT_CS_NEW - - self.host = host - self.port = port - self.keepalive = keepalive - self._bind_address = bind_address - self._bind_port = bind_port - self._clean_start = clean_start - self._connect_properties = properties - self._state = _ConnectionState.MQTT_CS_CONNECT_ASYNC - - def reconnect_delay_set(self, min_delay: int = 1, max_delay: int = 120) -> None: - """ Configure the exponential reconnect delay - - When connection is lost, wait initially min_delay seconds and - double this time every attempt. The wait is capped at max_delay. - Once the client is fully connected (e.g. not only TCP socket, but - received a success CONNACK), the wait timer is reset to min_delay. - """ - with self._reconnect_delay_mutex: - self._reconnect_min_delay = min_delay - self._reconnect_max_delay = max_delay - self._reconnect_delay = None - - def reconnect(self) -> MQTTErrorCode: - """Reconnect the client after a disconnect. Can only be called after - connect()/connect_async().""" - if len(self._host) == 0: - raise ValueError('Invalid host.') - if self._port <= 0: - raise ValueError('Invalid port number.') - - self._in_packet = { - "command": 0, - "have_remaining": 0, - "remaining_count": [], - "remaining_mult": 1, - "remaining_length": 0, - "packet": bytearray(b""), - "to_process": 0, - "pos": 0, - } - - self._ping_t = 0.0 - self._state = _ConnectionState.MQTT_CS_CONNECTING - - self._sock_close() - - # Mark all currently outgoing QoS = 0 packets as lost, - # or `wait_for_publish()` could hang forever - for pkt in self._out_packet: - if pkt["command"] & 0xF0 == PUBLISH and pkt["qos"] == 0 and pkt["info"] is not None: - pkt["info"].rc = MQTT_ERR_CONN_LOST - pkt["info"]._set_as_published() - - self._out_packet.clear() - - with self._msgtime_mutex: - self._last_msg_in = time_func() - self._last_msg_out = time_func() - - # Put messages in progress in a valid state. - self._messages_reconnect_reset() - - with self._callback_mutex: - on_pre_connect = self.on_pre_connect - - if on_pre_connect: - try: - on_pre_connect(self, self._userdata) - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_pre_connect: %s', err) - if not self.suppress_exceptions: - raise - - self._sock = self._create_socket() - - self._sock.setblocking(False) # type: ignore[attr-defined] - self._registered_write = False - self._call_socket_open(self._sock) - - return self._send_connect(self._keepalive) - - def loop(self, timeout: float = 1.0) -> MQTTErrorCode: - """Process network events. - - It is strongly recommended that you use `loop_start()`, or - `loop_forever()`, or if you are using an external event loop using - `loop_read()`, `loop_write()`, and `loop_misc()`. Using loop() on it's own is - no longer recommended. - - This function must be called regularly to ensure communication with the - broker is carried out. It calls select() on the network socket to wait - for network events. If incoming data is present it will then be - processed. Outgoing commands, from e.g. `publish()`, are normally sent - immediately that their function is called, but this is not always - possible. loop() will also attempt to send any remaining outgoing - messages, which also includes commands that are part of the flow for - messages with QoS>0. - - :param int timeout: The time in seconds to wait for incoming/outgoing network - traffic before timing out and returning. - - Returns MQTT_ERR_SUCCESS on success. - Returns >0 on error. - - A ValueError will be raised if timeout < 0""" - - if self._sockpairR is None or self._sockpairW is None: - self._reset_sockets(sockpair_only=True) - self._sockpairR, self._sockpairW = _socketpair_compat() - - return self._loop(timeout) - - def _loop(self, timeout: float = 1.0) -> MQTTErrorCode: - if timeout < 0.0: - raise ValueError('Invalid timeout.') - - if self.want_write(): - wlist = [self._sock] - else: - wlist = [] - - # used to check if there are any bytes left in the (SSL) socket - pending_bytes = 0 - if hasattr(self._sock, 'pending'): - pending_bytes = self._sock.pending() # type: ignore[union-attr] - - # if bytes are pending do not wait in select - if pending_bytes > 0: - timeout = 0.0 - - # sockpairR is used to break out of select() before the timeout, on a - # call to publish() etc. - if self._sockpairR is None: - rlist = [self._sock] - else: - rlist = [self._sock, self._sockpairR] - - try: - socklist = select.select(rlist, wlist, [], timeout) - except TypeError: - # Socket isn't correct type, in likelihood connection is lost - # ... or we called disconnect(). In that case the socket will - # be closed but some loop (like loop_forever) will continue to - # call _loop(). We still want to break that loop by returning an - # rc != MQTT_ERR_SUCCESS and we don't want state to change from - # mqtt_cs_disconnecting. - if self._state not in (_ConnectionState.MQTT_CS_DISCONNECTING, _ConnectionState.MQTT_CS_DISCONNECTED): - self._state = _ConnectionState.MQTT_CS_CONNECTION_LOST - return MQTTErrorCode.MQTT_ERR_CONN_LOST - except ValueError: - # Can occur if we just reconnected but rlist/wlist contain a -1 for - # some reason. - if self._state not in (_ConnectionState.MQTT_CS_DISCONNECTING, _ConnectionState.MQTT_CS_DISCONNECTED): - self._state = _ConnectionState.MQTT_CS_CONNECTION_LOST - return MQTTErrorCode.MQTT_ERR_CONN_LOST - except Exception: - # Note that KeyboardInterrupt, etc. can still terminate since they - # are not derived from Exception - return MQTTErrorCode.MQTT_ERR_UNKNOWN - - if self._sock in socklist[0] or pending_bytes > 0: - rc = self.loop_read() - if rc or self._sock is None: - return rc - - if self._sockpairR and self._sockpairR in socklist[0]: - # Stimulate output write even though we didn't ask for it, because - # at that point the publish or other command wasn't present. - socklist[1].insert(0, self._sock) - # Clear sockpairR - only ever a single byte written. - try: - # Read many bytes at once - this allows up to 10000 calls to - # publish() inbetween calls to loop(). - self._sockpairR.recv(10000) - except BlockingIOError: - pass - - if self._sock in socklist[1]: - rc = self.loop_write() - if rc or self._sock is None: - return rc - - return self.loop_misc() - - def publish( - self, - topic: str, - payload: PayloadType = None, - qos: int = 0, - retain: bool = False, - properties: Properties | None = None, - ) -> MQTTMessageInfo: - """Publish a message on a topic. - - This causes a message to be sent to the broker and subsequently from - the broker to any clients subscribing to matching topics. - - :param str topic: The topic that the message should be published on. - :param payload: The actual message to send. If not given, or set to None a - zero length message will be used. Passing an int or float will result - in the payload being converted to a string representing that number. If - you wish to send a true int/float, use struct.pack() to create the - payload you require. - :param int qos: The quality of service level to use. - :param bool retain: If set to true, the message will be set as the "last known - good"/retained message for the topic. - :param Properties properties: (MQTT v5.0 only) the MQTT v5.0 properties to be included. - - Returns a `MQTTMessageInfo` class, which can be used to determine whether - the message has been delivered (using `is_published()`) or to block - waiting for the message to be delivered (`wait_for_publish()`). The - message ID and return code of the publish() call can be found at - :py:attr:`info.mid ` and :py:attr:`info.rc `. - - For backwards compatibility, the `MQTTMessageInfo` class is iterable so - the old construct of ``(rc, mid) = client.publish(...)`` is still valid. - - rc is MQTT_ERR_SUCCESS to indicate success or MQTT_ERR_NO_CONN if the - client is not currently connected. mid is the message ID for the - publish request. The mid value can be used to track the publish request - by checking against the mid argument in the on_publish() callback if it - is defined. - - :raises ValueError: if topic is None, has zero length or is - invalid (contains a wildcard), except if the MQTT version used is v5.0. - For v5.0, a zero length topic can be used when a Topic Alias has been set. - :raises ValueError: if qos is not one of 0, 1 or 2 - :raises ValueError: if the length of the payload is greater than 268435455 bytes. - """ - if self._protocol != MQTTv5: - if topic is None or len(topic) == 0: - raise ValueError('Invalid topic.') - - topic_bytes = topic.encode('utf-8') - - self._raise_for_invalid_topic(topic_bytes) - - if qos < 0 or qos > 2: - raise ValueError('Invalid QoS level.') - - local_payload = _encode_payload(payload) - - if len(local_payload) > 268435455: - raise ValueError('Payload too large.') - - local_mid = self._mid_generate() - - if qos == 0: - info = MQTTMessageInfo(local_mid) - rc = self._send_publish( - local_mid, topic_bytes, local_payload, qos, retain, False, info, properties) - info.rc = rc - return info - else: - message = MQTTMessage(local_mid, topic_bytes) - message.timestamp = time_func() - message.payload = local_payload - message.qos = qos - message.retain = retain - message.dup = False - message.properties = properties - - with self._out_message_mutex: - if self._max_queued_messages > 0 and len(self._out_messages) >= self._max_queued_messages: - message.info.rc = MQTTErrorCode.MQTT_ERR_QUEUE_SIZE - return message.info - - if local_mid in self._out_messages: - message.info.rc = MQTTErrorCode.MQTT_ERR_QUEUE_SIZE - return message.info - - self._out_messages[message.mid] = message - if self._max_inflight_messages == 0 or self._inflight_messages < self._max_inflight_messages: - self._inflight_messages += 1 - if qos == 1: - message.state = mqtt_ms_wait_for_puback - elif qos == 2: - message.state = mqtt_ms_wait_for_pubrec - - rc = self._send_publish(message.mid, topic_bytes, message.payload, message.qos, message.retain, - message.dup, message.info, message.properties) - - # remove from inflight messages so it will be send after a connection is made - if rc == MQTTErrorCode.MQTT_ERR_NO_CONN: - self._inflight_messages -= 1 - message.state = mqtt_ms_publish - - message.info.rc = rc - return message.info - else: - message.state = mqtt_ms_queued - message.info.rc = MQTTErrorCode.MQTT_ERR_SUCCESS - return message.info - - def username_pw_set( - self, username: str | None, password: str | None = None - ) -> None: - """Set a username and optionally a password for broker authentication. - - Must be called before connect() to have any effect. - Requires a broker that supports MQTT v3.1 or more. - - :param str username: The username to authenticate with. Need have no relationship to the client id. Must be str - [MQTT-3.1.3-11]. - Set to None to reset client back to not using username/password for broker authentication. - :param str password: The password to authenticate with. Optional, set to None if not required. If it is str, then it - will be encoded as UTF-8. - """ - - # [MQTT-3.1.3-11] User name must be UTF-8 encoded string - self._username = None if username is None else username.encode('utf-8') - if isinstance(password, str): - self._password = password.encode('utf-8') - else: - self._password = password - - def enable_bridge_mode(self) -> None: - """Sets the client in a bridge mode instead of client mode. - - Must be called before `connect()` to have any effect. - Requires brokers that support bridge mode. - - Under bridge mode, the broker will identify the client as a bridge and - not send it's own messages back to it. Hence a subsciption of # is - possible without message loops. This feature also correctly propagates - the retain flag on the messages. - - Currently Mosquitto and RSMB support this feature. This feature can - be used to create a bridge between multiple broker. - """ - self._client_mode = MQTT_BRIDGE - - def _connection_closed(self) -> bool: - """ - Return true if the connection is closed (and not trying to be opened). - """ - return ( - self._state == _ConnectionState.MQTT_CS_NEW - or (self._state in (_ConnectionState.MQTT_CS_DISCONNECTING, _ConnectionState.MQTT_CS_DISCONNECTED) and self._sock is None)) - - def is_connected(self) -> bool: - """Returns the current status of the connection - - True if connection exists - False if connection is closed - """ - return self._state == _ConnectionState.MQTT_CS_CONNECTED - - def disconnect( - self, - reasoncode: ReasonCode | None = None, - properties: Properties | None = None, - ) -> MQTTErrorCode: - """Disconnect a connected client from the broker. - - :param ReasonCode reasoncode: (MQTT v5.0 only) a ReasonCode instance setting the MQTT v5.0 - reasoncode to be sent with the disconnect packet. It is optional, the receiver - then assuming that 0 (success) is the value. - :param Properties properties: (MQTT v5.0 only) a Properties instance setting the MQTT v5.0 properties - to be included. Optional - if not set, no properties are sent. - """ - if self._sock is None: - self._state = _ConnectionState.MQTT_CS_DISCONNECTED - return MQTT_ERR_NO_CONN - else: - self._state = _ConnectionState.MQTT_CS_DISCONNECTING - - return self._send_disconnect(reasoncode, properties) - - def subscribe( - self, - topic: str | tuple[str, int] | tuple[str, SubscribeOptions] | list[tuple[str, int]] | list[tuple[str, SubscribeOptions]], - qos: int = 0, - options: SubscribeOptions | None = None, - properties: Properties | None = None, - ) -> tuple[MQTTErrorCode, int | None]: - """Subscribe the client to one or more topics. - - This function may be called in three different ways (and a further three for MQTT v5.0): - - Simple string and integer - ------------------------- - e.g. subscribe("my/topic", 2) - - :topic: A string specifying the subscription topic to subscribe to. - :qos: The desired quality of service level for the subscription. - Defaults to 0. - :options and properties: Not used. - - Simple string and subscribe options (MQTT v5.0 only) - ---------------------------------------------------- - e.g. subscribe("my/topic", options=SubscribeOptions(qos=2)) - - :topic: A string specifying the subscription topic to subscribe to. - :qos: Not used. - :options: The MQTT v5.0 subscribe options. - :properties: a Properties instance setting the MQTT v5.0 properties - to be included. Optional - if not set, no properties are sent. - - String and integer tuple - ------------------------ - e.g. subscribe(("my/topic", 1)) - - :topic: A tuple of (topic, qos). Both topic and qos must be present in - the tuple. - :qos and options: Not used. - :properties: Only used for MQTT v5.0. A Properties instance setting the - MQTT v5.0 properties. Optional - if not set, no properties are sent. - - String and subscribe options tuple (MQTT v5.0 only) - --------------------------------------------------- - e.g. subscribe(("my/topic", SubscribeOptions(qos=1))) - - :topic: A tuple of (topic, SubscribeOptions). Both topic and subscribe - options must be present in the tuple. - :qos and options: Not used. - :properties: a Properties instance setting the MQTT v5.0 properties - to be included. Optional - if not set, no properties are sent. - - List of string and integer tuples - --------------------------------- - e.g. subscribe([("my/topic", 0), ("another/topic", 2)]) - - This allows multiple topic subscriptions in a single SUBSCRIPTION - command, which is more efficient than using multiple calls to - subscribe(). - - :topic: A list of tuple of format (topic, qos). Both topic and qos must - be present in all of the tuples. - :qos, options and properties: Not used. - - List of string and subscribe option tuples (MQTT v5.0 only) - ----------------------------------------------------------- - e.g. subscribe([("my/topic", SubscribeOptions(qos=0), ("another/topic", SubscribeOptions(qos=2)]) - - This allows multiple topic subscriptions in a single SUBSCRIPTION - command, which is more efficient than using multiple calls to - subscribe(). - - :topic: A list of tuple of format (topic, SubscribeOptions). Both topic and subscribe - options must be present in all of the tuples. - :qos and options: Not used. - :properties: a Properties instance setting the MQTT v5.0 properties - to be included. Optional - if not set, no properties are sent. - - The function returns a tuple (result, mid), where result is - MQTT_ERR_SUCCESS to indicate success or (MQTT_ERR_NO_CONN, None) if the - client is not currently connected. mid is the message ID for the - subscribe request. The mid value can be used to track the subscribe - request by checking against the mid argument in the on_subscribe() - callback if it is defined. - - Raises a ValueError if qos is not 0, 1 or 2, or if topic is None or has - zero string length, or if topic is not a string, tuple or list. - """ - topic_qos_list = None - - if isinstance(topic, tuple): - if self._protocol == MQTTv5: - topic, options = topic # type: ignore - if not isinstance(options, SubscribeOptions): - raise ValueError( - 'Subscribe options must be instance of SubscribeOptions class.') - else: - topic, qos = topic # type: ignore - - if isinstance(topic, (bytes, str)): - if qos < 0 or qos > 2: - raise ValueError('Invalid QoS level.') - if self._protocol == MQTTv5: - if options is None: - # if no options are provided, use the QoS passed instead - options = SubscribeOptions(qos=qos) - elif qos != 0: - raise ValueError( - 'Subscribe options and qos parameters cannot be combined.') - if not isinstance(options, SubscribeOptions): - raise ValueError( - 'Subscribe options must be instance of SubscribeOptions class.') - topic_qos_list = [(topic.encode('utf-8'), options)] - else: - if topic is None or len(topic) == 0: - raise ValueError('Invalid topic.') - topic_qos_list = [(topic.encode('utf-8'), qos)] # type: ignore - elif isinstance(topic, list): - if len(topic) == 0: - raise ValueError('Empty topic list') - topic_qos_list = [] - if self._protocol == MQTTv5: - for t, o in topic: - if not isinstance(o, SubscribeOptions): - # then the second value should be QoS - if o < 0 or o > 2: - raise ValueError('Invalid QoS level.') - o = SubscribeOptions(qos=o) - topic_qos_list.append((t.encode('utf-8'), o)) - else: - for t, q in topic: - if isinstance(q, SubscribeOptions) or q < 0 or q > 2: - raise ValueError('Invalid QoS level.') - if t is None or len(t) == 0 or not isinstance(t, (bytes, str)): - raise ValueError('Invalid topic.') - topic_qos_list.append((t.encode('utf-8'), q)) # type: ignore - - if topic_qos_list is None: - raise ValueError("No topic specified, or incorrect topic type.") - - if any(self._filter_wildcard_len_check(topic) != MQTT_ERR_SUCCESS for topic, _ in topic_qos_list): - raise ValueError('Invalid subscription filter.') - - if self._sock is None: - return (MQTT_ERR_NO_CONN, None) - - return self._send_subscribe(False, topic_qos_list, properties) - - def unsubscribe( - self, topic: str | list[str], properties: Properties | None = None - ) -> tuple[MQTTErrorCode, int | None]: - """Unsubscribe the client from one or more topics. - - :param topic: A single string, or list of strings that are the subscription - topics to unsubscribe from. - :param properties: (MQTT v5.0 only) a Properties instance setting the MQTT v5.0 properties - to be included. Optional - if not set, no properties are sent. - - Returns a tuple (result, mid), where result is MQTT_ERR_SUCCESS - to indicate success or (MQTT_ERR_NO_CONN, None) if the client is not - currently connected. - mid is the message ID for the unsubscribe request. The mid value can be - used to track the unsubscribe request by checking against the mid - argument in the on_unsubscribe() callback if it is defined. - - :raises ValueError: if topic is None or has zero string length, or is - not a string or list. - """ - topic_list = None - if topic is None: - raise ValueError('Invalid topic.') - if isinstance(topic, (bytes, str)): - if len(topic) == 0: - raise ValueError('Invalid topic.') - topic_list = [topic.encode('utf-8')] - elif isinstance(topic, list): - topic_list = [] - for t in topic: - if len(t) == 0 or not isinstance(t, (bytes, str)): - raise ValueError('Invalid topic.') - topic_list.append(t.encode('utf-8')) - - if topic_list is None: - raise ValueError("No topic specified, or incorrect topic type.") - - if self._sock is None: - return (MQTTErrorCode.MQTT_ERR_NO_CONN, None) - - return self._send_unsubscribe(False, topic_list, properties) - - def loop_read(self, max_packets: int = 1) -> MQTTErrorCode: - """Process read network events. Use in place of calling `loop()` if you - wish to handle your client reads as part of your own application. - - Use `socket()` to obtain the client socket to call select() or equivalent - on. - - Do not use if you are using `loop_start()` or `loop_forever()`.""" - if self._sock is None: - return MQTTErrorCode.MQTT_ERR_NO_CONN - - max_packets = len(self._out_messages) + len(self._in_messages) - if max_packets < 1: - max_packets = 1 - - for _ in range(0, max_packets): - if self._sock is None: - return MQTTErrorCode.MQTT_ERR_NO_CONN - rc = self._packet_read() - if rc > 0: - return self._loop_rc_handle(rc) - elif rc == MQTTErrorCode.MQTT_ERR_AGAIN: - return MQTTErrorCode.MQTT_ERR_SUCCESS - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def loop_write(self) -> MQTTErrorCode: - """Process write network events. Use in place of calling `loop()` if you - wish to handle your client writes as part of your own application. - - Use `socket()` to obtain the client socket to call select() or equivalent - on. - - Use `want_write()` to determine if there is data waiting to be written. - - Do not use if you are using `loop_start()` or `loop_forever()`.""" - if self._sock is None: - return MQTTErrorCode.MQTT_ERR_NO_CONN - - try: - rc = self._packet_write() - if rc == MQTTErrorCode.MQTT_ERR_AGAIN: - return MQTTErrorCode.MQTT_ERR_SUCCESS - elif rc > 0: - return self._loop_rc_handle(rc) - else: - return MQTTErrorCode.MQTT_ERR_SUCCESS - finally: - if self.want_write(): - self._call_socket_register_write() - else: - self._call_socket_unregister_write() - - def want_write(self) -> bool: - """Call to determine if there is network data waiting to be written. - Useful if you are calling select() yourself rather than using `loop()`, `loop_start()` or `loop_forever()`. - """ - return len(self._out_packet) > 0 - - def loop_misc(self) -> MQTTErrorCode: - """Process miscellaneous network events. Use in place of calling `loop()` if you - wish to call select() or equivalent on. - - Do not use if you are using `loop_start()` or `loop_forever()`.""" - if self._sock is None: - return MQTTErrorCode.MQTT_ERR_NO_CONN - - now = time_func() - self._check_keepalive() - - if self._ping_t > 0 and now - self._ping_t >= self._keepalive: - # client->ping_t != 0 means we are waiting for a pingresp. - # This hasn't happened in the keepalive time so we should disconnect. - self._sock_close() - - if self._state in (_ConnectionState.MQTT_CS_DISCONNECTING, _ConnectionState.MQTT_CS_DISCONNECTED): - self._state = _ConnectionState.MQTT_CS_DISCONNECTED - rc = MQTTErrorCode.MQTT_ERR_SUCCESS - else: - self._state = _ConnectionState.MQTT_CS_CONNECTION_LOST - rc = MQTTErrorCode.MQTT_ERR_KEEPALIVE - - self._do_on_disconnect( - packet_from_broker=False, - v1_rc=rc, - ) - - return MQTTErrorCode.MQTT_ERR_CONN_LOST - - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def max_inflight_messages_set(self, inflight: int) -> None: - """Set the maximum number of messages with QoS>0 that can be part way - through their network flow at once. Defaults to 20.""" - self.max_inflight_messages = inflight - - def max_queued_messages_set(self, queue_size: int) -> Client: - """Set the maximum number of messages in the outgoing message queue. - 0 means unlimited.""" - if not isinstance(queue_size, int): - raise ValueError('Invalid type of queue size.') - self.max_queued_messages = queue_size - return self - - def user_data_set(self, userdata: Any) -> None: - """Set the user data variable passed to callbacks. May be any data type.""" - self._userdata = userdata - - def user_data_get(self) -> Any: - """Get the user data variable passed to callbacks. May be any data type.""" - return self._userdata - - def will_set( - self, - topic: str, - payload: PayloadType = None, - qos: int = 0, - retain: bool = False, - properties: Properties | None = None, - ) -> None: - """Set a Will to be sent by the broker in case the client disconnects unexpectedly. - - This must be called before connect() to have any effect. - - :param str topic: The topic that the will message should be published on. - :param payload: The message to send as a will. If not given, or set to None a - zero length message will be used as the will. Passing an int or float - will result in the payload being converted to a string representing - that number. If you wish to send a true int/float, use struct.pack() to - create the payload you require. - :param int qos: The quality of service level to use for the will. - :param bool retain: If set to true, the will message will be set as the "last known - good"/retained message for the topic. - :param Properties properties: (MQTT v5.0 only) the MQTT v5.0 properties - to be included with the will message. Optional - if not set, no properties are sent. - - :raises ValueError: if qos is not 0, 1 or 2, or if topic is None or has - zero string length. - - See `will_clear` to clear will. Note that will are NOT send if the client disconnect cleanly - for example by calling `disconnect()`. - """ - if topic is None or len(topic) == 0: - raise ValueError('Invalid topic.') - - if qos < 0 or qos > 2: - raise ValueError('Invalid QoS level.') - - if properties and not isinstance(properties, Properties): - raise ValueError( - "The properties argument must be an instance of the Properties class.") - - self._will_payload = _encode_payload(payload) - self._will = True - self._will_topic = topic.encode('utf-8') - self._will_qos = qos - self._will_retain = retain - self._will_properties = properties - - def will_clear(self) -> None: - """ Removes a will that was previously configured with `will_set()`. - - Must be called before connect() to have any effect.""" - self._will = False - self._will_topic = b"" - self._will_payload = b"" - self._will_qos = 0 - self._will_retain = False - - def socket(self) -> SocketLike | None: - """Return the socket or ssl object for this client.""" - return self._sock - - def loop_forever( - self, - timeout: float = 1.0, - retry_first_connection: bool = False, - ) -> MQTTErrorCode: - """This function calls the network loop functions for you in an - infinite blocking loop. It is useful for the case where you only want - to run the MQTT client loop in your program. - - loop_forever() will handle reconnecting for you if reconnect_on_failure is - true (this is the default behavior). If you call `disconnect()` in a callback - it will return. - - :param int timeout: The time in seconds to wait for incoming/outgoing network - traffic before timing out and returning. - :param bool retry_first_connection: Should the first connection attempt be retried on failure. - This is independent of the reconnect_on_failure setting. - - :raises OSError: if the first connection fail unless retry_first_connection=True - """ - - run = True - - while run: - if self._thread_terminate is True: - break - - if self._state == _ConnectionState.MQTT_CS_CONNECT_ASYNC: - try: - self.reconnect() - except OSError: - self._handle_on_connect_fail() - if not retry_first_connection: - raise - self._easy_log( - MQTT_LOG_DEBUG, "Connection failed, retrying") - self._reconnect_wait() - else: - break - - while run: - rc = MQTTErrorCode.MQTT_ERR_SUCCESS - while rc == MQTTErrorCode.MQTT_ERR_SUCCESS: - rc = self._loop(timeout) - # We don't need to worry about locking here, because we've - # either called loop_forever() when in single threaded mode, or - # in multi threaded mode when loop_stop() has been called and - # so no other threads can access _out_packet or _messages. - if (self._thread_terminate is True - and len(self._out_packet) == 0 - and len(self._out_messages) == 0): - rc = MQTTErrorCode.MQTT_ERR_NOMEM - run = False - - def should_exit() -> bool: - return ( - self._state in (_ConnectionState.MQTT_CS_DISCONNECTING, _ConnectionState.MQTT_CS_DISCONNECTED) or - run is False or # noqa: B023 (uses the run variable from the outer scope on purpose) - self._thread_terminate is True - ) - - if should_exit() or not self._reconnect_on_failure: - run = False - else: - self._reconnect_wait() - - if should_exit(): - run = False - else: - try: - self.reconnect() - except OSError: - self._handle_on_connect_fail() - self._easy_log( - MQTT_LOG_DEBUG, "Connection failed, retrying") - - return rc - - def loop_start(self) -> MQTTErrorCode: - """This is part of the threaded client interface. Call this once to - start a new thread to process network traffic. This provides an - alternative to repeatedly calling `loop()` yourself. - - Under the hood, this will call `loop_forever` in a thread, which means that - the thread will terminate if you call `disconnect()` - """ - if self._thread is not None: - return MQTTErrorCode.MQTT_ERR_INVAL - - self._sockpairR, self._sockpairW = _socketpair_compat() - self._thread_terminate = False - self._thread = threading.Thread(target=self._thread_main, name=f"paho-mqtt-client-{self._client_id.decode()}") - self._thread.daemon = True - self._thread.start() - - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def loop_stop(self) -> MQTTErrorCode: - """This is part of the threaded client interface. Call this once to - stop the network thread previously created with `loop_start()`. This call - will block until the network thread finishes. - - This don't guarantee that publish packet are sent, use `wait_for_publish` or - `on_publish` to ensure `publish` are sent. - """ - if self._thread is None: - return MQTTErrorCode.MQTT_ERR_INVAL - - self._thread_terminate = True - if threading.current_thread() != self._thread: - self._thread.join() - - return MQTTErrorCode.MQTT_ERR_SUCCESS - - @property - def callback_api_version(self) -> CallbackAPIVersion: - """ - Return the callback API version used for user-callback. See docstring for - each user-callback (`on_connect`, `on_publish`, ...) for details. - - This property is read-only. - """ - return self._callback_api_version - - @property - def on_log(self) -> CallbackOnLog | None: - """The callback called when the client has log information. - Defined to allow debugging. - - Expected signature is:: - - log_callback(client, userdata, level, buf) - - :param Client client: the client instance for this callback - :param userdata: the private user data as set in Client() or user_data_set() - :param int level: gives the severity of the message and will be one of - MQTT_LOG_INFO, MQTT_LOG_NOTICE, MQTT_LOG_WARNING, - MQTT_LOG_ERR, and MQTT_LOG_DEBUG. - :param str buf: the message itself - - Decorator: @client.log_callback() (``client`` is the name of the - instance which this callback is being attached to) - """ - return self._on_log - - @on_log.setter - def on_log(self, func: CallbackOnLog | None) -> None: - self._on_log = func - - def log_callback(self) -> Callable[[CallbackOnLog], CallbackOnLog]: - def decorator(func: CallbackOnLog) -> CallbackOnLog: - self.on_log = func - return func - return decorator - - @property - def on_pre_connect(self) -> CallbackOnPreConnect | None: - """The callback called immediately prior to the connection is made - request. - - Expected signature (for all callback API version):: - - connect_callback(client, userdata) - - :parama Client client: the client instance for this callback - :parama userdata: the private user data as set in Client() or user_data_set() - - Decorator: @client.pre_connect_callback() (``client`` is the name of the - instance which this callback is being attached to) - - """ - return self._on_pre_connect - - @on_pre_connect.setter - def on_pre_connect(self, func: CallbackOnPreConnect | None) -> None: - with self._callback_mutex: - self._on_pre_connect = func - - def pre_connect_callback( - self, - ) -> Callable[[CallbackOnPreConnect], CallbackOnPreConnect]: - def decorator(func: CallbackOnPreConnect) -> CallbackOnPreConnect: - self.on_pre_connect = func - return func - return decorator - - @property - def on_connect(self) -> CallbackOnConnect | None: - """The callback called when the broker reponds to our connection request. - - Expected signature for callback API version 2:: - - connect_callback(client, userdata, connect_flags, reason_code, properties) - - Expected signature for callback API version 1 change with MQTT protocol version: - * For MQTT v3.1 and v3.1.1 it's:: - - connect_callback(client, userdata, flags, rc) - - * For MQTT v5.0 it's:: - - connect_callback(client, userdata, flags, reason_code, properties) - - - :param Client client: the client instance for this callback - :param userdata: the private user data as set in Client() or user_data_set() - :param ConnectFlags connect_flags: the flags for this connection - :param ReasonCode reason_code: the connection reason code received from the broken. - In MQTT v5.0 it's the reason code defined by the standard. - In MQTT v3, we convert return code to a reason code, see - `convert_connack_rc_to_reason_code()`. - `ReasonCode` may be compared to integer. - :param Properties properties: the MQTT v5.0 properties received from the broker. - For MQTT v3.1 and v3.1.1 properties is not provided and an empty Properties - object is always used. - :param dict flags: response flags sent by the broker - :param int rc: the connection result, should have a value of `ConnackCode` - - flags is a dict that contains response flags from the broker: - flags['session present'] - this flag is useful for clients that are - using clean session set to 0 only. If a client with clean - session=0, that reconnects to a broker that it has previously - connected to, this flag indicates whether the broker still has the - session information for the client. If 1, the session still exists. - - The value of rc indicates success or not: - - 0: Connection successful - - 1: Connection refused - incorrect protocol version - - 2: Connection refused - invalid client identifier - - 3: Connection refused - server unavailable - - 4: Connection refused - bad username or password - - 5: Connection refused - not authorised - - 6-255: Currently unused. - - Decorator: @client.connect_callback() (``client`` is the name of the - instance which this callback is being attached to) - """ - return self._on_connect - - @on_connect.setter - def on_connect(self, func: CallbackOnConnect | None) -> None: - with self._callback_mutex: - self._on_connect = func - - def connect_callback( - self, - ) -> Callable[[CallbackOnConnect], CallbackOnConnect]: - def decorator(func: CallbackOnConnect) -> CallbackOnConnect: - self.on_connect = func - return func - return decorator - - @property - def on_connect_fail(self) -> CallbackOnConnectFail | None: - """The callback called when the client failed to connect - to the broker. - - Expected signature is (for all callback_api_version):: - - connect_fail_callback(client, userdata) - - :param Client client: the client instance for this callback - :parama userdata: the private user data as set in Client() or user_data_set() - - Decorator: @client.connect_fail_callback() (``client`` is the name of the - instance which this callback is being attached to) - """ - return self._on_connect_fail - - @on_connect_fail.setter - def on_connect_fail(self, func: CallbackOnConnectFail | None) -> None: - with self._callback_mutex: - self._on_connect_fail = func - - def connect_fail_callback( - self, - ) -> Callable[[CallbackOnConnectFail], CallbackOnConnectFail]: - def decorator(func: CallbackOnConnectFail) -> CallbackOnConnectFail: - self.on_connect_fail = func - return func - return decorator - - @property - def on_subscribe(self) -> CallbackOnSubscribe | None: - """The callback called when the broker responds to a subscribe - request. - - Expected signature for callback API version 2:: - - subscribe_callback(client, userdata, mid, reason_code_list, properties) - - Expected signature for callback API version 1 change with MQTT protocol version: - * For MQTT v3.1 and v3.1.1 it's:: - - subscribe_callback(client, userdata, mid, granted_qos) - - * For MQTT v5.0 it's:: - - subscribe_callback(client, userdata, mid, reason_code_list, properties) - - :param Client client: the client instance for this callback - :param userdata: the private user data as set in Client() or user_data_set() - :param int mid: matches the mid variable returned from the corresponding - subscribe() call. - :param list[ReasonCode] reason_code_list: reason codes received from the broker for each subscription. - In MQTT v5.0 it's the reason code defined by the standard. - In MQTT v3, we convert granted QoS to a reason code. - It's a list of ReasonCode instances. - :param Properties properties: the MQTT v5.0 properties received from the broker. - For MQTT v3.1 and v3.1.1 properties is not provided and an empty Properties - object is always used. - :param list[int] granted_qos: list of integers that give the QoS level the broker has - granted for each of the different subscription requests. - - Decorator: @client.subscribe_callback() (``client`` is the name of the - instance which this callback is being attached to) - """ - return self._on_subscribe - - @on_subscribe.setter - def on_subscribe(self, func: CallbackOnSubscribe | None) -> None: - with self._callback_mutex: - self._on_subscribe = func - - def subscribe_callback( - self, - ) -> Callable[[CallbackOnSubscribe], CallbackOnSubscribe]: - def decorator(func: CallbackOnSubscribe) -> CallbackOnSubscribe: - self.on_subscribe = func - return func - return decorator - - @property - def on_message(self) -> CallbackOnMessage | None: - """The callback called when a message has been received on a topic - that the client subscribes to. - - This callback will be called for every message received unless a - `message_callback_add()` matched the message. - - Expected signature is (for all callback API version): - message_callback(client, userdata, message) - - :param Client client: the client instance for this callback - :param userdata: the private user data as set in Client() or user_data_set() - :param MQTTMessage message: the received message. - This is a class with members topic, payload, qos, retain. - - Decorator: @client.message_callback() (``client`` is the name of the - instance which this callback is being attached to) - """ - return self._on_message - - @on_message.setter - def on_message(self, func: CallbackOnMessage | None) -> None: - with self._callback_mutex: - self._on_message = func - - def message_callback( - self, - ) -> Callable[[CallbackOnMessage], CallbackOnMessage]: - def decorator(func: CallbackOnMessage) -> CallbackOnMessage: - self.on_message = func - return func - return decorator - - @property - def on_publish(self) -> CallbackOnPublish | None: - """The callback called when a message that was to be sent using the - `publish()` call has completed transmission to the broker. - - For messages with QoS levels 1 and 2, this means that the appropriate - handshakes have completed. For QoS 0, this simply means that the message - has left the client. - This callback is important because even if the `publish()` call returns - success, it does not always mean that the message has been sent. - - See also `wait_for_publish` which could be simpler to use. - - Expected signature for callback API version 2:: - - publish_callback(client, userdata, mid, reason_code, properties) - - Expected signature for callback API version 1:: - - publish_callback(client, userdata, mid) - - :param Client client: the client instance for this callback - :param userdata: the private user data as set in Client() or user_data_set() - :param int mid: matches the mid variable returned from the corresponding - `publish()` call, to allow outgoing messages to be tracked. - :param ReasonCode reason_code: the connection reason code received from the broken. - In MQTT v5.0 it's the reason code defined by the standard. - In MQTT v3 it's always the reason code Success - :parama Properties properties: the MQTT v5.0 properties received from the broker. - For MQTT v3.1 and v3.1.1 properties is not provided and an empty Properties - object is always used. - - Note: for QoS = 0, the reason_code and the properties don't really exist, it's the client - library that generate them. It's always an empty properties and a success reason code. - Because the (MQTTv5) standard don't have reason code for PUBLISH packet, the library create them - at PUBACK packet, as if the message was sent with QoS = 1. - - Decorator: @client.publish_callback() (``client`` is the name of the - instance which this callback is being attached to) - - """ - return self._on_publish - - @on_publish.setter - def on_publish(self, func: CallbackOnPublish | None) -> None: - with self._callback_mutex: - self._on_publish = func - - def publish_callback( - self, - ) -> Callable[[CallbackOnPublish], CallbackOnPublish]: - def decorator(func: CallbackOnPublish) -> CallbackOnPublish: - self.on_publish = func - return func - return decorator - - @property - def on_unsubscribe(self) -> CallbackOnUnsubscribe | None: - """The callback called when the broker responds to an unsubscribe - request. - - Expected signature for callback API version 2:: - - unsubscribe_callback(client, userdata, mid, reason_code_list, properties) - - Expected signature for callback API version 1 change with MQTT protocol version: - * For MQTT v3.1 and v3.1.1 it's:: - - unsubscribe_callback(client, userdata, mid) - - * For MQTT v5.0 it's:: - - unsubscribe_callback(client, userdata, mid, properties, v1_reason_codes) - - :param Client client: the client instance for this callback - :param userdata: the private user data as set in Client() or user_data_set() - :param mid: matches the mid variable returned from the corresponding - unsubscribe() call. - :param list[ReasonCode] reason_code_list: reason codes received from the broker for each unsubscription. - In MQTT v5.0 it's the reason code defined by the standard. - In MQTT v3, there is not equivalent from broken and empty list - is always used. - :param Properties properties: the MQTT v5.0 properties received from the broker. - For MQTT v3.1 and v3.1.1 properties is not provided and an empty Properties - object is always used. - :param v1_reason_codes: the MQTT v5.0 reason codes received from the broker for each - unsubscribe topic. A list of ReasonCode instances OR a single - ReasonCode when we unsubscribe from a single topic. - - Decorator: @client.unsubscribe_callback() (``client`` is the name of the - instance which this callback is being attached to) - """ - return self._on_unsubscribe - - @on_unsubscribe.setter - def on_unsubscribe(self, func: CallbackOnUnsubscribe | None) -> None: - with self._callback_mutex: - self._on_unsubscribe = func - - def unsubscribe_callback( - self, - ) -> Callable[[CallbackOnUnsubscribe], CallbackOnUnsubscribe]: - def decorator(func: CallbackOnUnsubscribe) -> CallbackOnUnsubscribe: - self.on_unsubscribe = func - return func - return decorator - - @property - def on_disconnect(self) -> CallbackOnDisconnect | None: - """The callback called when the client disconnects from the broker. - - Expected signature for callback API version 2:: - - disconnect_callback(client, userdata, disconnect_flags, reason_code, properties) - - Expected signature for callback API version 1 change with MQTT protocol version: - * For MQTT v3.1 and v3.1.1 it's:: - - disconnect_callback(client, userdata, rc) - - * For MQTT v5.0 it's:: - - disconnect_callback(client, userdata, reason_code, properties) - - :param Client client: the client instance for this callback - :param userdata: the private user data as set in Client() or user_data_set() - :param DisconnectFlag disconnect_flags: the flags for this disconnection. - :param ReasonCode reason_code: the disconnection reason code possibly received from the broker (see disconnect_flags). - In MQTT v5.0 it's the reason code defined by the standard. - In MQTT v3 it's never received from the broker, we convert an MQTTErrorCode, - see `convert_disconnect_error_code_to_reason_code()`. - `ReasonCode` may be compared to integer. - :param Properties properties: the MQTT v5.0 properties received from the broker. - For MQTT v3.1 and v3.1.1 properties is not provided and an empty Properties - object is always used. - :param int rc: the disconnection result - The rc parameter indicates the disconnection state. If - MQTT_ERR_SUCCESS (0), the callback was called in response to - a disconnect() call. If any other value the disconnection - was unexpected, such as might be caused by a network error. - - Decorator: @client.disconnect_callback() (``client`` is the name of the - instance which this callback is being attached to) - - """ - return self._on_disconnect - - @on_disconnect.setter - def on_disconnect(self, func: CallbackOnDisconnect | None) -> None: - with self._callback_mutex: - self._on_disconnect = func - - def disconnect_callback( - self, - ) -> Callable[[CallbackOnDisconnect], CallbackOnDisconnect]: - def decorator(func: CallbackOnDisconnect) -> CallbackOnDisconnect: - self.on_disconnect = func - return func - return decorator - - @property - def on_socket_open(self) -> CallbackOnSocket | None: - """The callback called just after the socket was opend. - - This should be used to register the socket to an external event loop for reading. - - Expected signature is (for all callback API version):: - - socket_open_callback(client, userdata, socket) - - :param Client client: the client instance for this callback - :param userdata: the private user data as set in Client() or user_data_set() - :param SocketLike sock: the socket which was just opened. - - Decorator: @client.socket_open_callback() (``client`` is the name of the - instance which this callback is being attached to) - """ - return self._on_socket_open - - @on_socket_open.setter - def on_socket_open(self, func: CallbackOnSocket | None) -> None: - with self._callback_mutex: - self._on_socket_open = func - - def socket_open_callback( - self, - ) -> Callable[[CallbackOnSocket], CallbackOnSocket]: - def decorator(func: CallbackOnSocket) -> CallbackOnSocket: - self.on_socket_open = func - return func - return decorator - - def _call_socket_open(self, sock: SocketLike) -> None: - """Call the socket_open callback with the just-opened socket""" - with self._callback_mutex: - on_socket_open = self.on_socket_open - - if on_socket_open: - with self._in_callback_mutex: - try: - on_socket_open(self, self._userdata, sock) - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_socket_open: %s', err) - if not self.suppress_exceptions: - raise - - @property - def on_socket_close(self) -> CallbackOnSocket | None: - """The callback called just before the socket is closed. - - This should be used to unregister the socket from an external event loop for reading. - - Expected signature is (for all callback API version):: - - socket_close_callback(client, userdata, socket) - - :param Client client: the client instance for this callback - :param userdata: the private user data as set in Client() or user_data_set() - :param SocketLike sock: the socket which is about to be closed. - - Decorator: @client.socket_close_callback() (``client`` is the name of the - instance which this callback is being attached to) - """ - return self._on_socket_close - - @on_socket_close.setter - def on_socket_close(self, func: CallbackOnSocket | None) -> None: - with self._callback_mutex: - self._on_socket_close = func - - def socket_close_callback( - self, - ) -> Callable[[CallbackOnSocket], CallbackOnSocket]: - def decorator(func: CallbackOnSocket) -> CallbackOnSocket: - self.on_socket_close = func - return func - return decorator - - def _call_socket_close(self, sock: SocketLike) -> None: - """Call the socket_close callback with the about-to-be-closed socket""" - with self._callback_mutex: - on_socket_close = self.on_socket_close - - if on_socket_close: - with self._in_callback_mutex: - try: - on_socket_close(self, self._userdata, sock) - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_socket_close: %s', err) - if not self.suppress_exceptions: - raise - - @property - def on_socket_register_write(self) -> CallbackOnSocket | None: - """The callback called when the socket needs writing but can't. - - This should be used to register the socket with an external event loop for writing. - - Expected signature is (for all callback API version):: - - socket_register_write_callback(client, userdata, socket) - - :param Client client: the client instance for this callback - :param userdata: the private user data as set in Client() or user_data_set() - :param SocketLike sock: the socket which should be registered for writing - - Decorator: @client.socket_register_write_callback() (``client`` is the name of the - instance which this callback is being attached to) - """ - return self._on_socket_register_write - - @on_socket_register_write.setter - def on_socket_register_write(self, func: CallbackOnSocket | None) -> None: - with self._callback_mutex: - self._on_socket_register_write = func - - def socket_register_write_callback( - self, - ) -> Callable[[CallbackOnSocket], CallbackOnSocket]: - def decorator(func: CallbackOnSocket) -> CallbackOnSocket: - self._on_socket_register_write = func - return func - return decorator - - def _call_socket_register_write(self) -> None: - """Call the socket_register_write callback with the unwritable socket""" - if not self._sock or self._registered_write: - return - self._registered_write = True - with self._callback_mutex: - on_socket_register_write = self.on_socket_register_write - - if on_socket_register_write: - try: - on_socket_register_write( - self, self._userdata, self._sock) - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_socket_register_write: %s', err) - if not self.suppress_exceptions: - raise - - @property - def on_socket_unregister_write( - self, - ) -> CallbackOnSocket | None: - """The callback called when the socket doesn't need writing anymore. - - This should be used to unregister the socket from an external event loop for writing. - - Expected signature is (for all callback API version):: - - socket_unregister_write_callback(client, userdata, socket) - - :param Client client: the client instance for this callback - :param userdata: the private user data as set in Client() or user_data_set() - :param SocketLike sock: the socket which should be unregistered for writing - - Decorator: @client.socket_unregister_write_callback() (``client`` is the name of the - instance which this callback is being attached to) - """ - return self._on_socket_unregister_write - - @on_socket_unregister_write.setter - def on_socket_unregister_write( - self, func: CallbackOnSocket | None - ) -> None: - with self._callback_mutex: - self._on_socket_unregister_write = func - - def socket_unregister_write_callback( - self, - ) -> Callable[[CallbackOnSocket], CallbackOnSocket]: - def decorator( - func: CallbackOnSocket, - ) -> CallbackOnSocket: - self._on_socket_unregister_write = func - return func - return decorator - - def _call_socket_unregister_write( - self, sock: SocketLike | None = None - ) -> None: - """Call the socket_unregister_write callback with the writable socket""" - sock = sock or self._sock - if not sock or not self._registered_write: - return - self._registered_write = False - - with self._callback_mutex: - on_socket_unregister_write = self.on_socket_unregister_write - - if on_socket_unregister_write: - try: - on_socket_unregister_write(self, self._userdata, sock) - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_socket_unregister_write: %s', err) - if not self.suppress_exceptions: - raise - - def message_callback_add(self, sub: str, callback: CallbackOnMessage) -> None: - """Register a message callback for a specific topic. - Messages that match 'sub' will be passed to 'callback'. Any - non-matching messages will be passed to the default `on_message` - callback. - - Call multiple times with different 'sub' to define multiple topic - specific callbacks. - - Topic specific callbacks may be removed with - `message_callback_remove()`. - - See `on_message` for the expected signature of the callback. - - Decorator: @client.topic_callback(sub) (``client`` is the name of the - instance which this callback is being attached to) - - Example:: - - @client.topic_callback("mytopic/#") - def handle_mytopic(client, userdata, message): - ... - """ - if callback is None or sub is None: - raise ValueError("sub and callback must both be defined.") - - with self._callback_mutex: - self._on_message_filtered[sub] = callback - - def topic_callback( - self, sub: str - ) -> Callable[[CallbackOnMessage], CallbackOnMessage]: - def decorator(func: CallbackOnMessage) -> CallbackOnMessage: - self.message_callback_add(sub, func) - return func - return decorator - - def message_callback_remove(self, sub: str) -> None: - """Remove a message callback previously registered with - `message_callback_add()`.""" - if sub is None: - raise ValueError("sub must defined.") - - with self._callback_mutex: - try: - del self._on_message_filtered[sub] - except KeyError: # no such subscription - pass - - # ============================================================ - # Private functions - # ============================================================ - - def _loop_rc_handle( - self, - rc: MQTTErrorCode, - ) -> MQTTErrorCode: - if rc: - self._sock_close() - - if self._state in (_ConnectionState.MQTT_CS_DISCONNECTING, _ConnectionState.MQTT_CS_DISCONNECTED): - self._state = _ConnectionState.MQTT_CS_DISCONNECTED - rc = MQTTErrorCode.MQTT_ERR_SUCCESS - - self._do_on_disconnect(packet_from_broker=False, v1_rc=rc) - - if rc == MQTT_ERR_CONN_LOST: - self._state = _ConnectionState.MQTT_CS_CONNECTION_LOST - - return rc - - def _packet_read(self) -> MQTTErrorCode: - # This gets called if pselect() indicates that there is network data - # available - ie. at least one byte. What we do depends on what data we - # already have. - # If we've not got a command, attempt to read one and save it. This should - # always work because it's only a single byte. - # Then try to read the remaining length. This may fail because it is may - # be more than one byte - will need to save data pending next read if it - # does fail. - # Then try to read the remaining payload, where 'payload' here means the - # combined variable header and actual payload. This is the most likely to - # fail due to longer length, so save current data and current position. - # After all data is read, send to _mqtt_handle_packet() to deal with. - # Finally, free the memory and reset everything to starting conditions. - if self._in_packet['command'] == 0: - try: - command = self._sock_recv(1) - except BlockingIOError: - return MQTTErrorCode.MQTT_ERR_AGAIN - except TimeoutError as err: - self._easy_log( - MQTT_LOG_ERR, 'timeout on socket: %s', err) - return MQTTErrorCode.MQTT_ERR_CONN_LOST - except OSError as err: - self._easy_log( - MQTT_LOG_ERR, 'failed to receive on socket: %s', err) - return MQTTErrorCode.MQTT_ERR_CONN_LOST - else: - if len(command) == 0: - return MQTTErrorCode.MQTT_ERR_CONN_LOST - self._in_packet['command'] = command[0] - - if self._in_packet['have_remaining'] == 0: - # Read remaining - # Algorithm for decoding taken from pseudo code at - # http://publib.boulder.ibm.com/infocenter/wmbhelp/v6r0m0/topic/com.ibm.etools.mft.doc/ac10870_.htm - while True: - try: - byte = self._sock_recv(1) - except BlockingIOError: - return MQTTErrorCode.MQTT_ERR_AGAIN - except OSError as err: - self._easy_log( - MQTT_LOG_ERR, 'failed to receive on socket: %s', err) - return MQTTErrorCode.MQTT_ERR_CONN_LOST - else: - if len(byte) == 0: - return MQTTErrorCode.MQTT_ERR_CONN_LOST - byte_value = byte[0] - self._in_packet['remaining_count'].append(byte_value) - # Max 4 bytes length for remaining length as defined by protocol. - # Anything more likely means a broken/malicious client. - if len(self._in_packet['remaining_count']) > 4: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - - self._in_packet['remaining_length'] += ( - byte_value & 127) * self._in_packet['remaining_mult'] - self._in_packet['remaining_mult'] = self._in_packet['remaining_mult'] * 128 - - if (byte_value & 128) == 0: - break - - self._in_packet['have_remaining'] = 1 - self._in_packet['to_process'] = self._in_packet['remaining_length'] - - count = 100 # Don't get stuck in this loop if we have a huge message. - while self._in_packet['to_process'] > 0: - try: - data = self._sock_recv(self._in_packet['to_process']) - except BlockingIOError: - return MQTTErrorCode.MQTT_ERR_AGAIN - except OSError as err: - self._easy_log( - MQTT_LOG_ERR, 'failed to receive on socket: %s', err) - return MQTTErrorCode.MQTT_ERR_CONN_LOST - else: - if len(data) == 0: - return MQTTErrorCode.MQTT_ERR_CONN_LOST - self._in_packet['to_process'] -= len(data) - self._in_packet['packet'] += data - count -= 1 - if count == 0: - with self._msgtime_mutex: - self._last_msg_in = time_func() - return MQTTErrorCode.MQTT_ERR_AGAIN - - # All data for this packet is read. - self._in_packet['pos'] = 0 - rc = self._packet_handle() - - # Free data and reset values - self._in_packet = { - "command": 0, - "have_remaining": 0, - "remaining_count": [], - "remaining_mult": 1, - "remaining_length": 0, - "packet": bytearray(b""), - "to_process": 0, - "pos": 0, - } - - with self._msgtime_mutex: - self._last_msg_in = time_func() - return rc - - def _packet_write(self) -> MQTTErrorCode: - while True: - try: - packet = self._out_packet.popleft() - except IndexError: - return MQTTErrorCode.MQTT_ERR_SUCCESS - - try: - write_length = self._sock_send( - packet['packet'][packet['pos']:]) - except (AttributeError, ValueError): - self._out_packet.appendleft(packet) - return MQTTErrorCode.MQTT_ERR_SUCCESS - except BlockingIOError: - self._out_packet.appendleft(packet) - return MQTTErrorCode.MQTT_ERR_AGAIN - except OSError as err: - self._out_packet.appendleft(packet) - self._easy_log( - MQTT_LOG_ERR, 'failed to receive on socket: %s', err) - return MQTTErrorCode.MQTT_ERR_CONN_LOST - - if write_length > 0: - packet['to_process'] -= write_length - packet['pos'] += write_length - - if packet['to_process'] == 0: - if (packet['command'] & 0xF0) == PUBLISH and packet['qos'] == 0: - with self._callback_mutex: - on_publish = self.on_publish - - if on_publish: - with self._in_callback_mutex: - try: - if self._callback_api_version == CallbackAPIVersion.VERSION1: - on_publish = cast(CallbackOnPublish_v1, on_publish) - - on_publish(self, self._userdata, packet["mid"]) - elif self._callback_api_version == CallbackAPIVersion.VERSION2: - on_publish = cast(CallbackOnPublish_v2, on_publish) - - on_publish( - self, - self._userdata, - packet["mid"], - ReasonCode(PacketTypes.PUBACK), - Properties(PacketTypes.PUBACK), - ) - else: - raise RuntimeError("Unsupported callback API version") - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_publish: %s', err) - if not self.suppress_exceptions: - raise - - # TODO: Something is odd here. I don't see why packet["info"] can't be None. - # A packet could be produced by _handle_connack with qos=0 and no info - # (around line 3645). Ignore the mypy check for now but I feel there is a bug - # somewhere. - packet['info']._set_as_published() # type: ignore - - if (packet['command'] & 0xF0) == DISCONNECT: - with self._msgtime_mutex: - self._last_msg_out = time_func() - - self._do_on_disconnect( - packet_from_broker=False, - v1_rc=MQTTErrorCode.MQTT_ERR_SUCCESS, - ) - self._sock_close() - # Only change to disconnected if the disconnection was wanted - # by the client (== state was disconnecting). If the broker disconnected - # use unilaterally don't change the state and client may reconnect. - if self._state == _ConnectionState.MQTT_CS_DISCONNECTING: - self._state = _ConnectionState.MQTT_CS_DISCONNECTED - return MQTTErrorCode.MQTT_ERR_SUCCESS - - else: - # We haven't finished with this packet - self._out_packet.appendleft(packet) - else: - break - - with self._msgtime_mutex: - self._last_msg_out = time_func() - - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def _easy_log(self, level: LogLevel, fmt: str, *args: Any) -> None: - if self.on_log is not None: - buf = fmt % args - try: - self.on_log(self, self._userdata, level, buf) - except Exception: # noqa: S110 - # Can't _easy_log this, as we'll recurse until we break - pass # self._logger will pick this up, so we're fine - if self._logger is not None: - level_std = LOGGING_LEVEL[level] - self._logger.log(level_std, fmt, *args) - - def _check_keepalive(self) -> None: - if self._keepalive == 0: - return - - now = time_func() - - with self._msgtime_mutex: - last_msg_out = self._last_msg_out - last_msg_in = self._last_msg_in - - if self._sock is not None and (now - last_msg_out >= self._keepalive or now - last_msg_in >= self._keepalive): - if self._state == _ConnectionState.MQTT_CS_CONNECTED and self._ping_t == 0: - try: - self._send_pingreq() - except Exception: - self._sock_close() - self._do_on_disconnect( - packet_from_broker=False, - v1_rc=MQTTErrorCode.MQTT_ERR_CONN_LOST, - ) - else: - with self._msgtime_mutex: - self._last_msg_out = now - self._last_msg_in = now - else: - self._sock_close() - - if self._state in (_ConnectionState.MQTT_CS_DISCONNECTING, _ConnectionState.MQTT_CS_DISCONNECTED): - self._state = _ConnectionState.MQTT_CS_DISCONNECTED - rc = MQTTErrorCode.MQTT_ERR_SUCCESS - else: - rc = MQTTErrorCode.MQTT_ERR_KEEPALIVE - - self._do_on_disconnect( - packet_from_broker=False, - v1_rc=rc, - ) - - def _mid_generate(self) -> int: - with self._mid_generate_mutex: - self._last_mid += 1 - if self._last_mid == 65536: - self._last_mid = 1 - return self._last_mid - - @staticmethod - def _raise_for_invalid_topic(topic: bytes) -> None: - """ Check if the topic is a topic without wildcard and valid length. - - Raise ValueError if the topic isn't valid. - """ - if b'+' in topic or b'#' in topic: - raise ValueError('Publish topic cannot contain wildcards.') - if len(topic) > 65535: - raise ValueError('Publish topic is too long.') - - @staticmethod - def _filter_wildcard_len_check(sub: bytes) -> MQTTErrorCode: - if (len(sub) == 0 or len(sub) > 65535 - or any(b'+' in p or b'#' in p for p in sub.split(b'/') if len(p) > 1) - or b'#/' in sub): - return MQTTErrorCode.MQTT_ERR_INVAL - else: - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def _send_pingreq(self) -> MQTTErrorCode: - self._easy_log(MQTT_LOG_DEBUG, "Sending PINGREQ") - rc = self._send_simple_command(PINGREQ) - if rc == MQTTErrorCode.MQTT_ERR_SUCCESS: - self._ping_t = time_func() - return rc - - def _send_pingresp(self) -> MQTTErrorCode: - self._easy_log(MQTT_LOG_DEBUG, "Sending PINGRESP") - return self._send_simple_command(PINGRESP) - - def _send_puback(self, mid: int) -> MQTTErrorCode: - self._easy_log(MQTT_LOG_DEBUG, "Sending PUBACK (Mid: %d)", mid) - return self._send_command_with_mid(PUBACK, mid, False) - - def _send_pubcomp(self, mid: int) -> MQTTErrorCode: - self._easy_log(MQTT_LOG_DEBUG, "Sending PUBCOMP (Mid: %d)", mid) - return self._send_command_with_mid(PUBCOMP, mid, False) - - def _pack_remaining_length( - self, packet: bytearray, remaining_length: int - ) -> bytearray: - remaining_bytes = [] - while True: - byte = remaining_length % 128 - remaining_length = remaining_length // 128 - # If there are more digits to encode, set the top bit of this digit - if remaining_length > 0: - byte |= 0x80 - - remaining_bytes.append(byte) - packet.append(byte) - if remaining_length == 0: - # FIXME - this doesn't deal with incorrectly large payloads - return packet - - def _pack_str16(self, packet: bytearray, data: bytes | str) -> None: - data = _force_bytes(data) - packet.extend(struct.pack("!H", len(data))) - packet.extend(data) - - def _send_publish( - self, - mid: int, - topic: bytes, - payload: bytes|bytearray = b"", - qos: int = 0, - retain: bool = False, - dup: bool = False, - info: MQTTMessageInfo | None = None, - properties: Properties | None = None, - ) -> MQTTErrorCode: - # we assume that topic and payload are already properly encoded - if not isinstance(topic, bytes): - raise TypeError('topic must be bytes, not str') - if payload and not isinstance(payload, (bytes, bytearray)): - raise TypeError('payload must be bytes if set') - - if self._sock is None: - return MQTTErrorCode.MQTT_ERR_NO_CONN - - command = PUBLISH | ((dup & 0x1) << 3) | (qos << 1) | retain - packet = bytearray() - packet.append(command) - - payloadlen = len(payload) - remaining_length = 2 + len(topic) + payloadlen - - if payloadlen == 0: - if self._protocol == MQTTv5: - self._easy_log( - MQTT_LOG_DEBUG, - "Sending PUBLISH (d%d, q%d, r%d, m%d), '%s', properties=%s (NULL payload)", - dup, qos, retain, mid, topic, properties - ) - else: - self._easy_log( - MQTT_LOG_DEBUG, - "Sending PUBLISH (d%d, q%d, r%d, m%d), '%s' (NULL payload)", - dup, qos, retain, mid, topic - ) - else: - if self._protocol == MQTTv5: - self._easy_log( - MQTT_LOG_DEBUG, - "Sending PUBLISH (d%d, q%d, r%d, m%d), '%s', properties=%s, ... (%d bytes)", - dup, qos, retain, mid, topic, properties, payloadlen - ) - else: - self._easy_log( - MQTT_LOG_DEBUG, - "Sending PUBLISH (d%d, q%d, r%d, m%d), '%s', ... (%d bytes)", - dup, qos, retain, mid, topic, payloadlen - ) - - if qos > 0: - # For message id - remaining_length += 2 - - if self._protocol == MQTTv5: - if properties is None: - packed_properties = b'\x00' - else: - packed_properties = properties.pack() - remaining_length += len(packed_properties) - - self._pack_remaining_length(packet, remaining_length) - self._pack_str16(packet, topic) - - if qos > 0: - # For message id - packet.extend(struct.pack("!H", mid)) - - if self._protocol == MQTTv5: - packet.extend(packed_properties) - - packet.extend(payload) - - return self._packet_queue(PUBLISH, packet, mid, qos, info) - - def _send_pubrec(self, mid: int) -> MQTTErrorCode: - self._easy_log(MQTT_LOG_DEBUG, "Sending PUBREC (Mid: %d)", mid) - return self._send_command_with_mid(PUBREC, mid, False) - - def _send_pubrel(self, mid: int) -> MQTTErrorCode: - self._easy_log(MQTT_LOG_DEBUG, "Sending PUBREL (Mid: %d)", mid) - return self._send_command_with_mid(PUBREL | 2, mid, False) - - def _send_command_with_mid(self, command: int, mid: int, dup: int) -> MQTTErrorCode: - # For PUBACK, PUBCOMP, PUBREC, and PUBREL - if dup: - command |= 0x8 - - remaining_length = 2 - packet = struct.pack('!BBH', command, remaining_length, mid) - return self._packet_queue(command, packet, mid, 1) - - def _send_simple_command(self, command: int) -> MQTTErrorCode: - # For DISCONNECT, PINGREQ and PINGRESP - remaining_length = 0 - packet = struct.pack('!BB', command, remaining_length) - return self._packet_queue(command, packet, 0, 0) - - def _send_connect(self, keepalive: int) -> MQTTErrorCode: - proto_ver = int(self._protocol) - # hard-coded UTF-8 encoded string - protocol = b"MQTT" if proto_ver >= MQTTv311 else b"MQIsdp" - - remaining_length = 2 + len(protocol) + 1 + \ - 1 + 2 + 2 + len(self._client_id) - - connect_flags = 0 - if self._protocol == MQTTv5: - if self._clean_start is True: - connect_flags |= 0x02 - elif self._clean_start == MQTT_CLEAN_START_FIRST_ONLY and self._mqttv5_first_connect: - connect_flags |= 0x02 - elif self._clean_session: - connect_flags |= 0x02 - - if self._will: - remaining_length += 2 + \ - len(self._will_topic) + 2 + len(self._will_payload) - connect_flags |= 0x04 | ((self._will_qos & 0x03) << 3) | ( - (self._will_retain & 0x01) << 5) - - if self._username is not None: - remaining_length += 2 + len(self._username) - connect_flags |= 0x80 - if self._password is not None: - connect_flags |= 0x40 - remaining_length += 2 + len(self._password) - - if self._protocol == MQTTv5: - if self._connect_properties is None: - packed_connect_properties = b'\x00' - else: - packed_connect_properties = self._connect_properties.pack() - remaining_length += len(packed_connect_properties) - if self._will: - if self._will_properties is None: - packed_will_properties = b'\x00' - else: - packed_will_properties = self._will_properties.pack() - remaining_length += len(packed_will_properties) - - command = CONNECT - packet = bytearray() - packet.append(command) - - # as per the mosquitto broker, if the MSB of this version is set - # to 1, then it treats the connection as a bridge - if self._client_mode == MQTT_BRIDGE: - proto_ver |= 0x80 - - self._pack_remaining_length(packet, remaining_length) - packet.extend(struct.pack( - f"!H{len(protocol)}sBBH", - len(protocol), protocol, proto_ver, connect_flags, keepalive, - )) - - if self._protocol == MQTTv5: - packet += packed_connect_properties - - self._pack_str16(packet, self._client_id) - - if self._will: - if self._protocol == MQTTv5: - packet += packed_will_properties - self._pack_str16(packet, self._will_topic) - self._pack_str16(packet, self._will_payload) - - if self._username is not None: - self._pack_str16(packet, self._username) - - if self._password is not None: - self._pack_str16(packet, self._password) - - self._keepalive = keepalive - if self._protocol == MQTTv5: - self._easy_log( - MQTT_LOG_DEBUG, - "Sending CONNECT (u%d, p%d, wr%d, wq%d, wf%d, c%d, k%d) client_id=%s properties=%s", - (connect_flags & 0x80) >> 7, - (connect_flags & 0x40) >> 6, - (connect_flags & 0x20) >> 5, - (connect_flags & 0x18) >> 3, - (connect_flags & 0x4) >> 2, - (connect_flags & 0x2) >> 1, - keepalive, - self._client_id, - self._connect_properties - ) - else: - self._easy_log( - MQTT_LOG_DEBUG, - "Sending CONNECT (u%d, p%d, wr%d, wq%d, wf%d, c%d, k%d) client_id=%s", - (connect_flags & 0x80) >> 7, - (connect_flags & 0x40) >> 6, - (connect_flags & 0x20) >> 5, - (connect_flags & 0x18) >> 3, - (connect_flags & 0x4) >> 2, - (connect_flags & 0x2) >> 1, - keepalive, - self._client_id - ) - return self._packet_queue(command, packet, 0, 0) - - def _send_disconnect( - self, - reasoncode: ReasonCode | None = None, - properties: Properties | None = None, - ) -> MQTTErrorCode: - if self._protocol == MQTTv5: - self._easy_log(MQTT_LOG_DEBUG, "Sending DISCONNECT reasonCode=%s properties=%s", - reasoncode, - properties - ) - else: - self._easy_log(MQTT_LOG_DEBUG, "Sending DISCONNECT") - - remaining_length = 0 - - command = DISCONNECT - packet = bytearray() - packet.append(command) - - if self._protocol == MQTTv5: - if properties is not None or reasoncode is not None: - if reasoncode is None: - reasoncode = ReasonCode(DISCONNECT >> 4, identifier=0) - remaining_length += 1 - if properties is not None: - packed_props = properties.pack() - remaining_length += len(packed_props) - - self._pack_remaining_length(packet, remaining_length) - - if self._protocol == MQTTv5: - if reasoncode is not None: - packet += reasoncode.pack() - if properties is not None: - packet += packed_props - - return self._packet_queue(command, packet, 0, 0) - - def _send_subscribe( - self, - dup: int, - topics: Sequence[tuple[bytes, SubscribeOptions | int]], - properties: Properties | None = None, - ) -> tuple[MQTTErrorCode, int]: - remaining_length = 2 - if self._protocol == MQTTv5: - if properties is None: - packed_subscribe_properties = b'\x00' - else: - packed_subscribe_properties = properties.pack() - remaining_length += len(packed_subscribe_properties) - for t, _ in topics: - remaining_length += 2 + len(t) + 1 - - command = SUBSCRIBE | (dup << 3) | 0x2 - packet = bytearray() - packet.append(command) - self._pack_remaining_length(packet, remaining_length) - local_mid = self._mid_generate() - packet.extend(struct.pack("!H", local_mid)) - - if self._protocol == MQTTv5: - packet += packed_subscribe_properties - - for t, q in topics: - self._pack_str16(packet, t) - if self._protocol == MQTTv5: - packet += q.pack() # type: ignore - else: - packet.append(q) # type: ignore - - self._easy_log( - MQTT_LOG_DEBUG, - "Sending SUBSCRIBE (d%d, m%d) %s", - dup, - local_mid, - topics, - ) - return (self._packet_queue(command, packet, local_mid, 1), local_mid) - - def _send_unsubscribe( - self, - dup: int, - topics: list[bytes], - properties: Properties | None = None, - ) -> tuple[MQTTErrorCode, int]: - remaining_length = 2 - if self._protocol == MQTTv5: - if properties is None: - packed_unsubscribe_properties = b'\x00' - else: - packed_unsubscribe_properties = properties.pack() - remaining_length += len(packed_unsubscribe_properties) - for t in topics: - remaining_length += 2 + len(t) - - command = UNSUBSCRIBE | (dup << 3) | 0x2 - packet = bytearray() - packet.append(command) - self._pack_remaining_length(packet, remaining_length) - local_mid = self._mid_generate() - packet.extend(struct.pack("!H", local_mid)) - - if self._protocol == MQTTv5: - packet += packed_unsubscribe_properties - - for t in topics: - self._pack_str16(packet, t) - - # topics_repr = ", ".join("'"+topic.decode('utf8')+"'" for topic in topics) - if self._protocol == MQTTv5: - self._easy_log( - MQTT_LOG_DEBUG, - "Sending UNSUBSCRIBE (d%d, m%d) %s %s", - dup, - local_mid, - properties, - topics, - ) - else: - self._easy_log( - MQTT_LOG_DEBUG, - "Sending UNSUBSCRIBE (d%d, m%d) %s", - dup, - local_mid, - topics, - ) - return (self._packet_queue(command, packet, local_mid, 1), local_mid) - - def _check_clean_session(self) -> bool: - if self._protocol == MQTTv5: - if self._clean_start == MQTT_CLEAN_START_FIRST_ONLY: - return self._mqttv5_first_connect - else: - return self._clean_start # type: ignore - else: - return self._clean_session - - def _messages_reconnect_reset_out(self) -> None: - with self._out_message_mutex: - self._inflight_messages = 0 - for m in self._out_messages.values(): - m.timestamp = 0 - if self._max_inflight_messages == 0 or self._inflight_messages < self._max_inflight_messages: - if m.qos == 0: - m.state = mqtt_ms_publish - elif m.qos == 1: - # self._inflight_messages = self._inflight_messages + 1 - if m.state == mqtt_ms_wait_for_puback: - m.dup = True - m.state = mqtt_ms_publish - elif m.qos == 2: - # self._inflight_messages = self._inflight_messages + 1 - if self._check_clean_session(): - if m.state != mqtt_ms_publish: - m.dup = True - m.state = mqtt_ms_publish - else: - if m.state == mqtt_ms_wait_for_pubcomp: - m.state = mqtt_ms_resend_pubrel - else: - if m.state == mqtt_ms_wait_for_pubrec: - m.dup = True - m.state = mqtt_ms_publish - else: - m.state = mqtt_ms_queued - - def _messages_reconnect_reset_in(self) -> None: - with self._in_message_mutex: - if self._check_clean_session(): - self._in_messages = collections.OrderedDict() - return - for m in self._in_messages.values(): - m.timestamp = 0 - if m.qos != 2: - self._in_messages.pop(m.mid) - else: - # Preserve current state - pass - - def _messages_reconnect_reset(self) -> None: - self._messages_reconnect_reset_out() - self._messages_reconnect_reset_in() - - def _packet_queue( - self, - command: int, - packet: bytes, - mid: int, - qos: int, - info: MQTTMessageInfo | None = None, - ) -> MQTTErrorCode: - mpkt: _OutPacket = { - "command": command, - "mid": mid, - "qos": qos, - "pos": 0, - "to_process": len(packet), - "packet": packet, - "info": info, - } - - self._out_packet.append(mpkt) - - # Write a single byte to sockpairW (connected to sockpairR) to break - # out of select() if in threaded mode. - if self._sockpairW is not None: - try: - self._sockpairW.send(sockpair_data) - except BlockingIOError: - pass - - # If we have an external event loop registered, use that instead - # of calling loop_write() directly. - if self._thread is None and self._on_socket_register_write is None: - if self._in_callback_mutex.acquire(False): - self._in_callback_mutex.release() - return self.loop_write() - - self._call_socket_register_write() - - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def _packet_handle(self) -> MQTTErrorCode: - cmd = self._in_packet['command'] & 0xF0 - if cmd == PINGREQ: - return self._handle_pingreq() - elif cmd == PINGRESP: - return self._handle_pingresp() - elif cmd == PUBACK: - return self._handle_pubackcomp("PUBACK") - elif cmd == PUBCOMP: - return self._handle_pubackcomp("PUBCOMP") - elif cmd == PUBLISH: - return self._handle_publish() - elif cmd == PUBREC: - return self._handle_pubrec() - elif cmd == PUBREL: - return self._handle_pubrel() - elif cmd == CONNACK: - return self._handle_connack() - elif cmd == SUBACK: - self._handle_suback() - return MQTTErrorCode.MQTT_ERR_SUCCESS - elif cmd == UNSUBACK: - return self._handle_unsuback() - elif cmd == DISCONNECT and self._protocol == MQTTv5: # only allowed in MQTT 5.0 - self._handle_disconnect() - return MQTTErrorCode.MQTT_ERR_SUCCESS - else: - # If we don't recognise the command, return an error straight away. - self._easy_log(MQTT_LOG_ERR, "Error: Unrecognised command %s", cmd) - return MQTTErrorCode.MQTT_ERR_PROTOCOL - - def _handle_pingreq(self) -> MQTTErrorCode: - if self._in_packet['remaining_length'] != 0: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - - self._easy_log(MQTT_LOG_DEBUG, "Received PINGREQ") - return self._send_pingresp() - - def _handle_pingresp(self) -> MQTTErrorCode: - if self._in_packet['remaining_length'] != 0: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - - # No longer waiting for a PINGRESP. - self._ping_t = 0 - self._easy_log(MQTT_LOG_DEBUG, "Received PINGRESP") - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def _handle_connack(self) -> MQTTErrorCode: - if self._protocol == MQTTv5: - if self._in_packet['remaining_length'] < 2: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - elif self._in_packet['remaining_length'] != 2: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - - if self._protocol == MQTTv5: - (flags, result) = struct.unpack( - "!BB", self._in_packet['packet'][:2]) - if result == 1: - # This is probably a failure from a broker that doesn't support - # MQTT v5. - reason = ReasonCode(CONNACK >> 4, aName="Unsupported protocol version") - properties = None - else: - reason = ReasonCode(CONNACK >> 4, identifier=result) - properties = Properties(CONNACK >> 4) - properties.unpack(self._in_packet['packet'][2:]) - else: - (flags, result) = struct.unpack("!BB", self._in_packet['packet']) - reason = convert_connack_rc_to_reason_code(result) - properties = None - if self._protocol == MQTTv311: - if result == CONNACK_REFUSED_PROTOCOL_VERSION: - if not self._reconnect_on_failure: - return MQTT_ERR_PROTOCOL - self._easy_log( - MQTT_LOG_DEBUG, - "Received CONNACK (%s, %s), attempting downgrade to MQTT v3.1.", - flags, result - ) - # Downgrade to MQTT v3.1 - self._protocol = MQTTv31 - return self.reconnect() - elif (result == CONNACK_REFUSED_IDENTIFIER_REJECTED - and self._client_id == b''): - if not self._reconnect_on_failure: - return MQTT_ERR_PROTOCOL - self._easy_log( - MQTT_LOG_DEBUG, - "Received CONNACK (%s, %s), attempting to use non-empty CID", - flags, result, - ) - self._client_id = _base62(uuid.uuid4().int, padding=22).encode("utf8") - return self.reconnect() - - if result == 0: - self._state = _ConnectionState.MQTT_CS_CONNECTED - self._reconnect_delay = None - - if self._protocol == MQTTv5: - self._easy_log( - MQTT_LOG_DEBUG, "Received CONNACK (%s, %s) properties=%s", flags, reason, properties) - else: - self._easy_log( - MQTT_LOG_DEBUG, "Received CONNACK (%s, %s)", flags, result) - - # it won't be the first successful connect any more - self._mqttv5_first_connect = False - - with self._callback_mutex: - on_connect = self.on_connect - - if on_connect: - flags_dict = {} - flags_dict['session present'] = flags & 0x01 - with self._in_callback_mutex: - try: - if self._callback_api_version == CallbackAPIVersion.VERSION1: - if self._protocol == MQTTv5: - on_connect = cast(CallbackOnConnect_v1_mqtt5, on_connect) - - on_connect(self, self._userdata, - flags_dict, reason, properties) - else: - on_connect = cast(CallbackOnConnect_v1_mqtt3, on_connect) - - on_connect( - self, self._userdata, flags_dict, result) - elif self._callback_api_version == CallbackAPIVersion.VERSION2: - on_connect = cast(CallbackOnConnect_v2, on_connect) - - connect_flags = ConnectFlags( - session_present=flags_dict['session present'] > 0 - ) - - if properties is None: - properties = Properties(PacketTypes.CONNACK) - - on_connect( - self, - self._userdata, - connect_flags, - reason, - properties, - ) - else: - raise RuntimeError("Unsupported callback API version") - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_connect: %s', err) - if not self.suppress_exceptions: - raise - - if result == 0: - rc = MQTTErrorCode.MQTT_ERR_SUCCESS - with self._out_message_mutex: - for m in self._out_messages.values(): - m.timestamp = time_func() - if m.state == mqtt_ms_queued: - self.loop_write() # Process outgoing messages that have just been queued up - return MQTT_ERR_SUCCESS - - if m.qos == 0: - with self._in_callback_mutex: # Don't call loop_write after _send_publish() - rc = self._send_publish( - m.mid, - m.topic.encode('utf-8'), - m.payload, - m.qos, - m.retain, - m.dup, - properties=m.properties - ) - if rc != MQTTErrorCode.MQTT_ERR_SUCCESS: - return rc - elif m.qos == 1: - if m.state == mqtt_ms_publish: - self._inflight_messages += 1 - m.state = mqtt_ms_wait_for_puback - with self._in_callback_mutex: # Don't call loop_write after _send_publish() - rc = self._send_publish( - m.mid, - m.topic.encode('utf-8'), - m.payload, - m.qos, - m.retain, - m.dup, - properties=m.properties - ) - if rc != MQTTErrorCode.MQTT_ERR_SUCCESS: - return rc - elif m.qos == 2: - if m.state == mqtt_ms_publish: - self._inflight_messages += 1 - m.state = mqtt_ms_wait_for_pubrec - with self._in_callback_mutex: # Don't call loop_write after _send_publish() - rc = self._send_publish( - m.mid, - m.topic.encode('utf-8'), - m.payload, - m.qos, - m.retain, - m.dup, - properties=m.properties - ) - if rc != MQTTErrorCode.MQTT_ERR_SUCCESS: - return rc - elif m.state == mqtt_ms_resend_pubrel: - self._inflight_messages += 1 - m.state = mqtt_ms_wait_for_pubcomp - with self._in_callback_mutex: # Don't call loop_write after _send_publish() - rc = self._send_pubrel(m.mid) - if rc != MQTTErrorCode.MQTT_ERR_SUCCESS: - return rc - self.loop_write() # Process outgoing messages that have just been queued up - - return rc - elif result > 0 and result < 6: - return MQTTErrorCode.MQTT_ERR_CONN_REFUSED - else: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - - def _handle_disconnect(self) -> None: - packet_type = DISCONNECT >> 4 - reasonCode = properties = None - if self._in_packet['remaining_length'] > 2: - reasonCode = ReasonCode(packet_type) - reasonCode.unpack(self._in_packet['packet']) - if self._in_packet['remaining_length'] > 3: - properties = Properties(packet_type) - props, props_len = properties.unpack( - self._in_packet['packet'][1:]) - self._easy_log(MQTT_LOG_DEBUG, "Received DISCONNECT %s %s", - reasonCode, - properties - ) - - self._sock_close() - self._do_on_disconnect( - packet_from_broker=True, - v1_rc=MQTTErrorCode.MQTT_ERR_SUCCESS, # If reason is absent (remaining length < 1), it means normal disconnection - reason=reasonCode, - properties=properties, - ) - - def _handle_suback(self) -> None: - self._easy_log(MQTT_LOG_DEBUG, "Received SUBACK") - pack_format = f"!H{len(self._in_packet['packet']) - 2}s" - (mid, packet) = struct.unpack(pack_format, self._in_packet['packet']) - - if self._protocol == MQTTv5: - properties = Properties(SUBACK >> 4) - props, props_len = properties.unpack(packet) - reasoncodes = [ReasonCode(SUBACK >> 4, identifier=c) for c in packet[props_len:]] - else: - pack_format = f"!{'B' * len(packet)}" - granted_qos = struct.unpack(pack_format, packet) - reasoncodes = [ReasonCode(SUBACK >> 4, identifier=c) for c in granted_qos] - properties = Properties(SUBACK >> 4) - - with self._callback_mutex: - on_subscribe = self.on_subscribe - - if on_subscribe: - with self._in_callback_mutex: # Don't call loop_write after _send_publish() - try: - if self._callback_api_version == CallbackAPIVersion.VERSION1: - if self._protocol == MQTTv5: - on_subscribe = cast(CallbackOnSubscribe_v1_mqtt5, on_subscribe) - - on_subscribe( - self, self._userdata, mid, reasoncodes, properties) - else: - on_subscribe = cast(CallbackOnSubscribe_v1_mqtt3, on_subscribe) - - on_subscribe( - self, self._userdata, mid, granted_qos) - elif self._callback_api_version == CallbackAPIVersion.VERSION2: - on_subscribe = cast(CallbackOnSubscribe_v2, on_subscribe) - - on_subscribe( - self, - self._userdata, - mid, - reasoncodes, - properties, - ) - else: - raise RuntimeError("Unsupported callback API version") - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_subscribe: %s', err) - if not self.suppress_exceptions: - raise - - def _handle_publish(self) -> MQTTErrorCode: - header = self._in_packet['command'] - message = MQTTMessage() - message.dup = ((header & 0x08) >> 3) != 0 - message.qos = (header & 0x06) >> 1 - message.retain = (header & 0x01) != 0 - - pack_format = f"!H{len(self._in_packet['packet']) - 2}s" - (slen, packet) = struct.unpack(pack_format, self._in_packet['packet']) - pack_format = f"!{slen}s{len(packet) - slen}s" - (topic, packet) = struct.unpack(pack_format, packet) - - if self._protocol != MQTTv5 and len(topic) == 0: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - - # Handle topics with invalid UTF-8 - # This replaces an invalid topic with a message and the hex - # representation of the topic for logging. When the user attempts to - # access message.topic in the callback, an exception will be raised. - try: - print_topic = topic.decode('utf-8') - except UnicodeDecodeError: - print_topic = f"TOPIC WITH INVALID UTF-8: {topic!r}" - - message.topic = topic - - if message.qos > 0: - pack_format = f"!H{len(packet) - 2}s" - (message.mid, packet) = struct.unpack(pack_format, packet) - - if self._protocol == MQTTv5: - message.properties = Properties(PUBLISH >> 4) - props, props_len = message.properties.unpack(packet) - packet = packet[props_len:] - - message.payload = packet - - if self._protocol == MQTTv5: - self._easy_log( - MQTT_LOG_DEBUG, - "Received PUBLISH (d%d, q%d, r%d, m%d), '%s', properties=%s, ... (%d bytes)", - message.dup, message.qos, message.retain, message.mid, - print_topic, message.properties, len(message.payload) - ) - else: - self._easy_log( - MQTT_LOG_DEBUG, - "Received PUBLISH (d%d, q%d, r%d, m%d), '%s', ... (%d bytes)", - message.dup, message.qos, message.retain, message.mid, - print_topic, len(message.payload) - ) - - message.timestamp = time_func() - if message.qos == 0: - self._handle_on_message(message) - return MQTTErrorCode.MQTT_ERR_SUCCESS - elif message.qos == 1: - self._handle_on_message(message) - if self._manual_ack: - return MQTTErrorCode.MQTT_ERR_SUCCESS - else: - return self._send_puback(message.mid) - elif message.qos == 2: - - rc = self._send_pubrec(message.mid) - - message.state = mqtt_ms_wait_for_pubrel - with self._in_message_mutex: - self._in_messages[message.mid] = message - - return rc - else: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - - def ack(self, mid: int, qos: int) -> MQTTErrorCode: - """ - send an acknowledgement for a given message id (stored in :py:attr:`message.mid `). - only useful in QoS>=1 and ``manual_ack=True`` (option of `Client`) - """ - if self._manual_ack : - if qos == 1: - return self._send_puback(mid) - elif qos == 2: - return self._send_pubcomp(mid) - - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def manual_ack_set(self, on: bool) -> None: - """ - The paho library normally acknowledges messages as soon as they are delivered to the caller. - If manual_ack is turned on, then the caller MUST manually acknowledge every message once - application processing is complete using `ack()` - """ - self._manual_ack = on - - - def _handle_pubrel(self) -> MQTTErrorCode: - if self._protocol == MQTTv5: - if self._in_packet['remaining_length'] < 2: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - elif self._in_packet['remaining_length'] != 2: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - - mid, = struct.unpack("!H", self._in_packet['packet'][:2]) - if self._protocol == MQTTv5: - if self._in_packet['remaining_length'] > 2: - reasonCode = ReasonCode(PUBREL >> 4) - reasonCode.unpack(self._in_packet['packet'][2:]) - if self._in_packet['remaining_length'] > 3: - properties = Properties(PUBREL >> 4) - props, props_len = properties.unpack( - self._in_packet['packet'][3:]) - self._easy_log(MQTT_LOG_DEBUG, "Received PUBREL (Mid: %d)", mid) - - with self._in_message_mutex: - if mid in self._in_messages: - # Only pass the message on if we have removed it from the queue - this - # prevents multiple callbacks for the same message. - message = self._in_messages.pop(mid) - self._handle_on_message(message) - self._inflight_messages -= 1 - if self._max_inflight_messages > 0: - with self._out_message_mutex: - rc = self._update_inflight() - if rc != MQTTErrorCode.MQTT_ERR_SUCCESS: - return rc - - # FIXME: this should only be done if the message is known - # If unknown it's a protocol error and we should close the connection. - # But since we don't have (on disk) persistence for the session, it - # is possible that we must known about this message. - # Choose to acknowledge this message (thus losing a message) but - # avoid hanging. See #284. - if self._manual_ack: - return MQTTErrorCode.MQTT_ERR_SUCCESS - else: - return self._send_pubcomp(mid) - - def _update_inflight(self) -> MQTTErrorCode: - # Dont lock message_mutex here - for m in self._out_messages.values(): - if self._inflight_messages < self._max_inflight_messages: - if m.qos > 0 and m.state == mqtt_ms_queued: - self._inflight_messages += 1 - if m.qos == 1: - m.state = mqtt_ms_wait_for_puback - elif m.qos == 2: - m.state = mqtt_ms_wait_for_pubrec - rc = self._send_publish( - m.mid, - m.topic.encode('utf-8'), - m.payload, - m.qos, - m.retain, - m.dup, - properties=m.properties, - ) - if rc != MQTTErrorCode.MQTT_ERR_SUCCESS: - return rc - else: - return MQTTErrorCode.MQTT_ERR_SUCCESS - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def _handle_pubrec(self) -> MQTTErrorCode: - if self._protocol == MQTTv5: - if self._in_packet['remaining_length'] < 2: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - elif self._in_packet['remaining_length'] != 2: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - - mid, = struct.unpack("!H", self._in_packet['packet'][:2]) - if self._protocol == MQTTv5: - if self._in_packet['remaining_length'] > 2: - reasonCode = ReasonCode(PUBREC >> 4) - reasonCode.unpack(self._in_packet['packet'][2:]) - if self._in_packet['remaining_length'] > 3: - properties = Properties(PUBREC >> 4) - props, props_len = properties.unpack( - self._in_packet['packet'][3:]) - self._easy_log(MQTT_LOG_DEBUG, "Received PUBREC (Mid: %d)", mid) - - with self._out_message_mutex: - if mid in self._out_messages: - msg = self._out_messages[mid] - msg.state = mqtt_ms_wait_for_pubcomp - msg.timestamp = time_func() - return self._send_pubrel(mid) - - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def _handle_unsuback(self) -> MQTTErrorCode: - if self._protocol == MQTTv5: - if self._in_packet['remaining_length'] < 4: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - elif self._in_packet['remaining_length'] != 2: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - - mid, = struct.unpack("!H", self._in_packet['packet'][:2]) - if self._protocol == MQTTv5: - packet = self._in_packet['packet'][2:] - properties = Properties(UNSUBACK >> 4) - props, props_len = properties.unpack(packet) - reasoncodes_list = [ - ReasonCode(UNSUBACK >> 4, identifier=c) - for c in packet[props_len:] - ] - else: - reasoncodes_list = [] - properties = Properties(UNSUBACK >> 4) - - self._easy_log(MQTT_LOG_DEBUG, "Received UNSUBACK (Mid: %d)", mid) - with self._callback_mutex: - on_unsubscribe = self.on_unsubscribe - - if on_unsubscribe: - with self._in_callback_mutex: - try: - if self._callback_api_version == CallbackAPIVersion.VERSION1: - if self._protocol == MQTTv5: - on_unsubscribe = cast(CallbackOnUnsubscribe_v1_mqtt5, on_unsubscribe) - - reasoncodes: ReasonCode | list[ReasonCode] = reasoncodes_list - if len(reasoncodes_list) == 1: - reasoncodes = reasoncodes_list[0] - - on_unsubscribe( - self, self._userdata, mid, properties, reasoncodes) - else: - on_unsubscribe = cast(CallbackOnUnsubscribe_v1_mqtt3, on_unsubscribe) - - on_unsubscribe(self, self._userdata, mid) - elif self._callback_api_version == CallbackAPIVersion.VERSION2: - on_unsubscribe = cast(CallbackOnUnsubscribe_v2, on_unsubscribe) - - if properties is None: - properties = Properties(PacketTypes.CONNACK) - - on_unsubscribe( - self, - self._userdata, - mid, - reasoncodes_list, - properties, - ) - else: - raise RuntimeError("Unsupported callback API version") - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_unsubscribe: %s', err) - if not self.suppress_exceptions: - raise - - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def _do_on_disconnect( - self, - packet_from_broker: bool, - v1_rc: MQTTErrorCode, - reason: ReasonCode | None = None, - properties: Properties | None = None, - ) -> None: - with self._callback_mutex: - on_disconnect = self.on_disconnect - - if on_disconnect: - with self._in_callback_mutex: - try: - if self._callback_api_version == CallbackAPIVersion.VERSION1: - if self._protocol == MQTTv5: - on_disconnect = cast(CallbackOnDisconnect_v1_mqtt5, on_disconnect) - - if packet_from_broker: - on_disconnect(self, self._userdata, reason, properties) - else: - on_disconnect(self, self._userdata, v1_rc, None) - else: - on_disconnect = cast(CallbackOnDisconnect_v1_mqtt3, on_disconnect) - - on_disconnect(self, self._userdata, v1_rc) - elif self._callback_api_version == CallbackAPIVersion.VERSION2: - on_disconnect = cast(CallbackOnDisconnect_v2, on_disconnect) - - disconnect_flags = DisconnectFlags( - is_disconnect_packet_from_server=packet_from_broker - ) - - if reason is None: - reason = convert_disconnect_error_code_to_reason_code(v1_rc) - - if properties is None: - properties = Properties(PacketTypes.DISCONNECT) - - on_disconnect( - self, - self._userdata, - disconnect_flags, - reason, - properties, - ) - else: - raise RuntimeError("Unsupported callback API version") - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_disconnect: %s', err) - if not self.suppress_exceptions: - raise - - def _do_on_publish(self, mid: int, reason_code: ReasonCode, properties: Properties) -> MQTTErrorCode: - with self._callback_mutex: - on_publish = self.on_publish - - if on_publish: - with self._in_callback_mutex: - try: - if self._callback_api_version == CallbackAPIVersion.VERSION1: - on_publish = cast(CallbackOnPublish_v1, on_publish) - - on_publish(self, self._userdata, mid) - elif self._callback_api_version == CallbackAPIVersion.VERSION2: - on_publish = cast(CallbackOnPublish_v2, on_publish) - - on_publish( - self, - self._userdata, - mid, - reason_code, - properties, - ) - else: - raise RuntimeError("Unsupported callback API version") - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_publish: %s', err) - if not self.suppress_exceptions: - raise - - msg = self._out_messages.pop(mid) - msg.info._set_as_published() - if msg.qos > 0: - self._inflight_messages -= 1 - if self._max_inflight_messages > 0: - rc = self._update_inflight() - if rc != MQTTErrorCode.MQTT_ERR_SUCCESS: - return rc - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def _handle_pubackcomp( - self, cmd: Literal['PUBACK'] | Literal['PUBCOMP'] - ) -> MQTTErrorCode: - if self._protocol == MQTTv5: - if self._in_packet['remaining_length'] < 2: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - elif self._in_packet['remaining_length'] != 2: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - - packet_type_enum = PUBACK if cmd == "PUBACK" else PUBCOMP - packet_type = packet_type_enum.value >> 4 - mid, = struct.unpack("!H", self._in_packet['packet'][:2]) - reasonCode = ReasonCode(packet_type) - properties = Properties(packet_type) - if self._protocol == MQTTv5: - if self._in_packet['remaining_length'] > 2: - reasonCode.unpack(self._in_packet['packet'][2:]) - if self._in_packet['remaining_length'] > 3: - props, props_len = properties.unpack( - self._in_packet['packet'][3:]) - self._easy_log(MQTT_LOG_DEBUG, "Received %s (Mid: %d)", cmd, mid) - - with self._out_message_mutex: - if mid in self._out_messages: - # Only inform the client the message has been sent once. - rc = self._do_on_publish(mid, reasonCode, properties) - return rc - - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def _handle_on_message(self, message: MQTTMessage) -> None: - - try: - topic = message.topic - except UnicodeDecodeError: - topic = None - - on_message_callbacks = [] - with self._callback_mutex: - if topic is not None: - on_message_callbacks = list(self._on_message_filtered.iter_match(message.topic)) - - if len(on_message_callbacks) == 0: - on_message = self.on_message - else: - on_message = None - - for callback in on_message_callbacks: - with self._in_callback_mutex: - try: - callback(self, self._userdata, message) - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, - 'Caught exception in user defined callback function %s: %s', - callback.__name__, - err - ) - if not self.suppress_exceptions: - raise - - if on_message: - with self._in_callback_mutex: - try: - on_message(self, self._userdata, message) - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_message: %s', err) - if not self.suppress_exceptions: - raise - - - def _handle_on_connect_fail(self) -> None: - with self._callback_mutex: - on_connect_fail = self.on_connect_fail - - if on_connect_fail: - with self._in_callback_mutex: - try: - on_connect_fail(self, self._userdata) - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_connect_fail: %s', err) - - def _thread_main(self) -> None: - try: - self.loop_forever(retry_first_connection=True) - finally: - self._thread = None - - def _reconnect_wait(self) -> None: - # See reconnect_delay_set for details - now = time_func() - with self._reconnect_delay_mutex: - if self._reconnect_delay is None: - self._reconnect_delay = self._reconnect_min_delay - else: - self._reconnect_delay = min( - self._reconnect_delay * 2, - self._reconnect_max_delay, - ) - - target_time = now + self._reconnect_delay - - remaining = target_time - now - while (self._state not in (_ConnectionState.MQTT_CS_DISCONNECTING, _ConnectionState.MQTT_CS_DISCONNECTED) - and not self._thread_terminate - and remaining > 0): - - time.sleep(min(remaining, 1)) - remaining = target_time - time_func() - - @staticmethod - def _proxy_is_valid(p) -> bool: # type: ignore[no-untyped-def] - def check(t, a) -> bool: # type: ignore[no-untyped-def] - return (socks is not None and - t in {socks.HTTP, socks.SOCKS4, socks.SOCKS5} and a) - - if isinstance(p, dict): - return check(p.get("proxy_type"), p.get("proxy_addr")) - elif isinstance(p, (list, tuple)): - return len(p) == 6 and check(p[0], p[1]) - else: - return False - - def _get_proxy(self) -> dict[str, Any] | None: - if socks is None: - return None - - # First, check if the user explicitly passed us a proxy to use - if self._proxy_is_valid(self._proxy): - return self._proxy - - # Next, check for an mqtt_proxy environment variable as long as the host - # we're trying to connect to isn't listed under the no_proxy environment - # variable (matches built-in module urllib's behavior) - if not (hasattr(urllib.request, "proxy_bypass") and - urllib.request.proxy_bypass(self._host)): - env_proxies = urllib.request.getproxies() - if "mqtt" in env_proxies: - parts = urllib.parse.urlparse(env_proxies["mqtt"]) - if parts.scheme == "http": - proxy = { - "proxy_type": socks.HTTP, - "proxy_addr": parts.hostname, - "proxy_port": parts.port - } - return proxy - elif parts.scheme == "socks": - proxy = { - "proxy_type": socks.SOCKS5, - "proxy_addr": parts.hostname, - "proxy_port": parts.port - } - return proxy - - # Finally, check if the user has monkeypatched the PySocks library with - # a default proxy - socks_default = socks.get_default_proxy() - if self._proxy_is_valid(socks_default): - proxy_keys = ("proxy_type", "proxy_addr", "proxy_port", - "proxy_rdns", "proxy_username", "proxy_password") - return dict(zip(proxy_keys, socks_default)) - - # If we didn't find a proxy through any of the above methods, return - # None to indicate that the connection should be handled normally - return None - - def _create_socket(self) -> SocketLike: - if self._transport == "unix": - sock = self._create_unix_socket_connection() - else: - sock = self._create_socket_connection() - - if self._ssl: - sock = self._ssl_wrap_socket(sock) - - if self._transport == "websockets": - sock.settimeout(self._keepalive) - return _WebsocketWrapper( - socket=sock, - host=self._host, - port=self._port, - is_ssl=self._ssl, - path=self._websocket_path, - extra_headers=self._websocket_extra_headers, - ) - - return sock - - def _create_unix_socket_connection(self) -> _socket.socket: - unix_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - unix_socket.connect(self._host) - return unix_socket - - def _create_socket_connection(self) -> _socket.socket: - proxy = self._get_proxy() - addr = (self._host, self._port) - source = (self._bind_address, self._bind_port) - - if proxy: - return socks.create_connection(addr, timeout=self._connect_timeout, source_address=source, **proxy) - else: - return socket.create_connection(addr, timeout=self._connect_timeout, source_address=source) - - def _ssl_wrap_socket(self, tcp_sock: _socket.socket) -> ssl.SSLSocket: - if self._ssl_context is None: - raise ValueError( - "Impossible condition. _ssl_context should never be None if _ssl is True" - ) - - verify_host = not self._tls_insecure - try: - # Try with server_hostname, even it's not supported in certain scenarios - ssl_sock = self._ssl_context.wrap_socket( - tcp_sock, - server_hostname=self._host, - do_handshake_on_connect=False, - ) - except ssl.CertificateError: - # CertificateError is derived from ValueError - raise - except ValueError: - # Python version requires SNI in order to handle server_hostname, but SNI is not available - ssl_sock = self._ssl_context.wrap_socket( - tcp_sock, - do_handshake_on_connect=False, - ) - else: - # If SSL context has already checked hostname, then don't need to do it again - if getattr(self._ssl_context, 'check_hostname', False): # type: ignore - verify_host = False - - ssl_sock.settimeout(self._keepalive) - ssl_sock.do_handshake() - - if verify_host: - # TODO: this type error is a true error: - # error: Module has no attribute "match_hostname" [attr-defined] - # Python 3.12 no longer have this method. - ssl.match_hostname(ssl_sock.getpeercert(), self._host) # type: ignore - - return ssl_sock - -class _WebsocketWrapper: - OPCODE_CONTINUATION = 0x0 - OPCODE_TEXT = 0x1 - OPCODE_BINARY = 0x2 - OPCODE_CONNCLOSE = 0x8 - OPCODE_PING = 0x9 - OPCODE_PONG = 0xa - - def __init__( - self, - socket: socket.socket | ssl.SSLSocket, - host: str, - port: int, - is_ssl: bool, - path: str, - extra_headers: WebSocketHeaders | None, - ): - self.connected = False - - self._ssl = is_ssl - self._host = host - self._port = port - self._socket = socket - self._path = path - - self._sendbuffer = bytearray() - self._readbuffer = bytearray() - - self._requested_size = 0 - self._payload_head = 0 - self._readbuffer_head = 0 - - self._do_handshake(extra_headers) - - def __del__(self) -> None: - self._sendbuffer = bytearray() - self._readbuffer = bytearray() - - def _do_handshake(self, extra_headers: WebSocketHeaders | None) -> None: - - sec_websocket_key = uuid.uuid4().bytes - sec_websocket_key = base64.b64encode(sec_websocket_key) - - if self._ssl: - default_port = 443 - http_schema = "https" - else: - default_port = 80 - http_schema = "http" - - if default_port == self._port: - host_port = f"{self._host}" - else: - host_port = f"{self._host}:{self._port}" - - websocket_headers = { - "Host": host_port, - "Upgrade": "websocket", - "Connection": "Upgrade", - "Origin": f"{http_schema}://{host_port}", - "Sec-WebSocket-Key": sec_websocket_key.decode("utf8"), - "Sec-Websocket-Version": "13", - "Sec-Websocket-Protocol": "mqtt", - } - - # This is checked in ws_set_options so it will either be None, a - # dictionary, or a callable - if isinstance(extra_headers, dict): - websocket_headers.update(extra_headers) - elif callable(extra_headers): - websocket_headers = extra_headers(websocket_headers) - - header = "\r\n".join([ - f"GET {self._path} HTTP/1.1", - "\r\n".join(f"{i}: {j}" for i, j in websocket_headers.items()), - "\r\n", - ]).encode("utf8") - - self._socket.send(header) - - has_secret = False - has_upgrade = False - - while True: - # read HTTP response header as lines - try: - byte = self._socket.recv(1) - except ConnectionResetError: - byte = b"" - - self._readbuffer.extend(byte) - - # line end - if byte == b"\n": - if len(self._readbuffer) > 2: - # check upgrade - if b"connection" in str(self._readbuffer).lower().encode('utf-8'): - if b"upgrade" not in str(self._readbuffer).lower().encode('utf-8'): - raise WebsocketConnectionError( - "WebSocket handshake error, connection not upgraded") - else: - has_upgrade = True - - # check key hash - if b"sec-websocket-accept" in str(self._readbuffer).lower().encode('utf-8'): - GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" - - server_hash_str = self._readbuffer.decode( - 'utf-8').split(": ", 1)[1] - server_hash = server_hash_str.strip().encode('utf-8') - - client_hash_key = sec_websocket_key.decode('utf-8') + GUID - # Use of SHA-1 is OK here; it's according to the Websocket spec. - client_hash_digest = hashlib.sha1(client_hash_key.encode('utf-8')) # noqa: S324 - client_hash = base64.b64encode(client_hash_digest.digest()) - - if server_hash != client_hash: - raise WebsocketConnectionError( - "WebSocket handshake error, invalid secret key") - else: - has_secret = True - else: - # ending linebreak - break - - # reset linebuffer - self._readbuffer = bytearray() - - # connection reset - elif not byte: - raise WebsocketConnectionError("WebSocket handshake error") - - if not has_upgrade or not has_secret: - raise WebsocketConnectionError("WebSocket handshake error") - - self._readbuffer = bytearray() - self.connected = True - - def _create_frame( - self, opcode: int, data: bytearray, do_masking: int = 1 - ) -> bytearray: - header = bytearray() - length = len(data) - - mask_key = bytearray(os.urandom(4)) - mask_flag = do_masking - - # 1 << 7 is the final flag, we don't send continuated data - header.append(1 << 7 | opcode) - - if length < 126: - header.append(mask_flag << 7 | length) - - elif length < 65536: - header.append(mask_flag << 7 | 126) - header += struct.pack("!H", length) - - elif length < 0x8000000000000001: - header.append(mask_flag << 7 | 127) - header += struct.pack("!Q", length) - - else: - raise ValueError("Maximum payload size is 2^63") - - if mask_flag == 1: - for index in range(length): - data[index] ^= mask_key[index % 4] - data = mask_key + data - - return header + data - - def _buffered_read(self, length: int) -> bytearray: - - # try to recv and store needed bytes - wanted_bytes = length - (len(self._readbuffer) - self._readbuffer_head) - if wanted_bytes > 0: - - data = self._socket.recv(wanted_bytes) - - if not data: - raise ConnectionAbortedError - else: - self._readbuffer.extend(data) - - if len(data) < wanted_bytes: - raise BlockingIOError - - self._readbuffer_head += length - return self._readbuffer[self._readbuffer_head - length:self._readbuffer_head] - - def _recv_impl(self, length: int) -> bytes: - - # try to decode websocket payload part from data - try: - - self._readbuffer_head = 0 - - result = b"" - - chunk_startindex = self._payload_head - chunk_endindex = self._payload_head + length - - header1 = self._buffered_read(1) - header2 = self._buffered_read(1) - - opcode = (header1[0] & 0x0f) - maskbit = (header2[0] & 0x80) == 0x80 - lengthbits = (header2[0] & 0x7f) - payload_length = lengthbits - mask_key = None - - # read length - if lengthbits == 0x7e: - - value = self._buffered_read(2) - payload_length, = struct.unpack("!H", value) - - elif lengthbits == 0x7f: - - value = self._buffered_read(8) - payload_length, = struct.unpack("!Q", value) - - # read mask - if maskbit: - mask_key = self._buffered_read(4) - - # if frame payload is shorter than the requested data, read only the possible part - readindex = chunk_endindex - if payload_length < readindex: - readindex = payload_length - - if readindex > 0: - # get payload chunk - payload = self._buffered_read(readindex) - - # unmask only the needed part - if mask_key is not None: - for index in range(chunk_startindex, readindex): - payload[index] ^= mask_key[index % 4] - - result = payload[chunk_startindex:readindex] - self._payload_head = readindex - else: - payload = bytearray() - - # check if full frame arrived and reset readbuffer and payloadhead if needed - if readindex == payload_length: - self._readbuffer = bytearray() - self._payload_head = 0 - - # respond to non-binary opcodes, their arrival is not guaranteed because of non-blocking sockets - if opcode == _WebsocketWrapper.OPCODE_CONNCLOSE: - frame = self._create_frame( - _WebsocketWrapper.OPCODE_CONNCLOSE, payload, 0) - self._socket.send(frame) - - if opcode == _WebsocketWrapper.OPCODE_PING: - frame = self._create_frame( - _WebsocketWrapper.OPCODE_PONG, payload, 0) - self._socket.send(frame) - - # This isn't *proper* handling of continuation frames, but given - # that we only support binary frames, it is *probably* good enough. - if (opcode == _WebsocketWrapper.OPCODE_BINARY or opcode == _WebsocketWrapper.OPCODE_CONTINUATION) \ - and payload_length > 0: - return result - else: - raise BlockingIOError - - except ConnectionError: - self.connected = False - return b'' - - def _send_impl(self, data: bytes) -> int: - - # if previous frame was sent successfully - if len(self._sendbuffer) == 0: - # create websocket frame - frame = self._create_frame( - _WebsocketWrapper.OPCODE_BINARY, bytearray(data)) - self._sendbuffer.extend(frame) - self._requested_size = len(data) - - # try to write out as much as possible - length = self._socket.send(self._sendbuffer) - - self._sendbuffer = self._sendbuffer[length:] - - if len(self._sendbuffer) == 0: - # buffer sent out completely, return with payload's size - return self._requested_size - else: - # couldn't send whole data, request the same data again with 0 as sent length - return 0 - - def recv(self, length: int) -> bytes: - return self._recv_impl(length) - - def read(self, length: int) -> bytes: - return self._recv_impl(length) - - def send(self, data: bytes) -> int: - return self._send_impl(data) - - def write(self, data: bytes) -> int: - return self._send_impl(data) - - def close(self) -> None: - self._socket.close() - - def fileno(self) -> int: - return self._socket.fileno() - - def pending(self) -> int: - # Fix for bug #131: a SSL socket may still have data available - # for reading without select() being aware of it. - if self._ssl: - return self._socket.pending() # type: ignore[union-attr] - else: - # normal socket rely only on select() - return 0 - - def setblocking(self, flag: bool) -> None: - self._socket.setblocking(flag) diff --git a/sbapp/mqtt/enums.py b/sbapp/mqtt/enums.py deleted file mode 100644 index 5428769..0000000 --- a/sbapp/mqtt/enums.py +++ /dev/null @@ -1,113 +0,0 @@ -import enum - - -class MQTTErrorCode(enum.IntEnum): - MQTT_ERR_AGAIN = -1 - MQTT_ERR_SUCCESS = 0 - MQTT_ERR_NOMEM = 1 - MQTT_ERR_PROTOCOL = 2 - MQTT_ERR_INVAL = 3 - MQTT_ERR_NO_CONN = 4 - MQTT_ERR_CONN_REFUSED = 5 - MQTT_ERR_NOT_FOUND = 6 - MQTT_ERR_CONN_LOST = 7 - MQTT_ERR_TLS = 8 - MQTT_ERR_PAYLOAD_SIZE = 9 - MQTT_ERR_NOT_SUPPORTED = 10 - MQTT_ERR_AUTH = 11 - MQTT_ERR_ACL_DENIED = 12 - MQTT_ERR_UNKNOWN = 13 - MQTT_ERR_ERRNO = 14 - MQTT_ERR_QUEUE_SIZE = 15 - MQTT_ERR_KEEPALIVE = 16 - - -class MQTTProtocolVersion(enum.IntEnum): - MQTTv31 = 3 - MQTTv311 = 4 - MQTTv5 = 5 - - -class CallbackAPIVersion(enum.Enum): - """Defined the arguments passed to all user-callback. - - See each callbacks for details: `on_connect`, `on_connect_fail`, `on_disconnect`, `on_message`, `on_publish`, - `on_subscribe`, `on_unsubscribe`, `on_log`, `on_socket_open`, `on_socket_close`, - `on_socket_register_write`, `on_socket_unregister_write` - """ - VERSION1 = 1 - """The version used with paho-mqtt 1.x before introducing CallbackAPIVersion. - - This version had different arguments depending if MQTTv5 or MQTTv3 was used. `Properties` & `ReasonCode` were missing - on some callback (apply only to MQTTv5). - - This version is deprecated and will be removed in version 3.0. - """ - VERSION2 = 2 - """ This version fix some of the shortcoming of previous version. - - Callback have the same signature if using MQTTv5 or MQTTv3. `ReasonCode` are used in MQTTv3. - """ - - -class MessageType(enum.IntEnum): - CONNECT = 0x10 - CONNACK = 0x20 - PUBLISH = 0x30 - PUBACK = 0x40 - PUBREC = 0x50 - PUBREL = 0x60 - PUBCOMP = 0x70 - SUBSCRIBE = 0x80 - SUBACK = 0x90 - UNSUBSCRIBE = 0xA0 - UNSUBACK = 0xB0 - PINGREQ = 0xC0 - PINGRESP = 0xD0 - DISCONNECT = 0xE0 - AUTH = 0xF0 - - -class LogLevel(enum.IntEnum): - MQTT_LOG_INFO = 0x01 - MQTT_LOG_NOTICE = 0x02 - MQTT_LOG_WARNING = 0x04 - MQTT_LOG_ERR = 0x08 - MQTT_LOG_DEBUG = 0x10 - - -class ConnackCode(enum.IntEnum): - CONNACK_ACCEPTED = 0 - CONNACK_REFUSED_PROTOCOL_VERSION = 1 - CONNACK_REFUSED_IDENTIFIER_REJECTED = 2 - CONNACK_REFUSED_SERVER_UNAVAILABLE = 3 - CONNACK_REFUSED_BAD_USERNAME_PASSWORD = 4 - CONNACK_REFUSED_NOT_AUTHORIZED = 5 - - -class _ConnectionState(enum.Enum): - MQTT_CS_NEW = enum.auto() - MQTT_CS_CONNECT_ASYNC = enum.auto() - MQTT_CS_CONNECTING = enum.auto() - MQTT_CS_CONNECTED = enum.auto() - MQTT_CS_CONNECTION_LOST = enum.auto() - MQTT_CS_DISCONNECTING = enum.auto() - MQTT_CS_DISCONNECTED = enum.auto() - - -class MessageState(enum.IntEnum): - MQTT_MS_INVALID = 0 - MQTT_MS_PUBLISH = 1 - MQTT_MS_WAIT_FOR_PUBACK = 2 - MQTT_MS_WAIT_FOR_PUBREC = 3 - MQTT_MS_RESEND_PUBREL = 4 - MQTT_MS_WAIT_FOR_PUBREL = 5 - MQTT_MS_RESEND_PUBCOMP = 6 - MQTT_MS_WAIT_FOR_PUBCOMP = 7 - MQTT_MS_SEND_PUBREC = 8 - MQTT_MS_QUEUED = 9 - - -class PahoClientMode(enum.IntEnum): - MQTT_CLIENT = 0 - MQTT_BRIDGE = 1 diff --git a/sbapp/mqtt/matcher.py b/sbapp/mqtt/matcher.py deleted file mode 100644 index b73c13a..0000000 --- a/sbapp/mqtt/matcher.py +++ /dev/null @@ -1,78 +0,0 @@ -class MQTTMatcher: - """Intended to manage topic filters including wildcards. - - Internally, MQTTMatcher use a prefix tree (trie) to store - values associated with filters, and has an iter_match() - method to iterate efficiently over all filters that match - some topic name.""" - - class Node: - __slots__ = '_children', '_content' - - def __init__(self): - self._children = {} - self._content = None - - def __init__(self): - self._root = self.Node() - - def __setitem__(self, key, value): - """Add a topic filter :key to the prefix tree - and associate it to :value""" - node = self._root - for sym in key.split('/'): - node = node._children.setdefault(sym, self.Node()) - node._content = value - - def __getitem__(self, key): - """Retrieve the value associated with some topic filter :key""" - try: - node = self._root - for sym in key.split('/'): - node = node._children[sym] - if node._content is None: - raise KeyError(key) - return node._content - except KeyError as ke: - raise KeyError(key) from ke - - def __delitem__(self, key): - """Delete the value associated with some topic filter :key""" - lst = [] - try: - parent, node = None, self._root - for k in key.split('/'): - parent, node = node, node._children[k] - lst.append((parent, k, node)) - # TODO - node._content = None - except KeyError as ke: - raise KeyError(key) from ke - else: # cleanup - for parent, k, node in reversed(lst): - if node._children or node._content is not None: - break - del parent._children[k] - - def iter_match(self, topic): - """Return an iterator on all values associated with filters - that match the :topic""" - lst = topic.split('/') - normal = not topic.startswith('$') - def rec(node, i=0): - if i == len(lst): - if node._content is not None: - yield node._content - else: - part = lst[i] - if part in node._children: - for content in rec(node._children[part], i + 1): - yield content - if '+' in node._children and (normal or i > 0): - for content in rec(node._children['+'], i + 1): - yield content - if '#' in node._children and (normal or i > 0): - content = node._children['#']._content - if content is not None: - yield content - return rec(self._root) diff --git a/sbapp/mqtt/packettypes.py b/sbapp/mqtt/packettypes.py deleted file mode 100644 index d205149..0000000 --- a/sbapp/mqtt/packettypes.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -******************************************************************* - Copyright (c) 2017, 2019 IBM Corp. - - All rights reserved. This program and the accompanying materials - are made available under the terms of the Eclipse Public License v2.0 - and Eclipse Distribution License v1.0 which accompany this distribution. - - The Eclipse Public License is available at - http://www.eclipse.org/legal/epl-v20.html - and the Eclipse Distribution License is available at - http://www.eclipse.org/org/documents/edl-v10.php. - - Contributors: - Ian Craggs - initial implementation and/or documentation -******************************************************************* -""" - - -class PacketTypes: - - """ - Packet types class. Includes the AUTH packet for MQTT v5.0. - - Holds constants for each packet type such as PacketTypes.PUBLISH - and packet name strings: PacketTypes.Names[PacketTypes.PUBLISH]. - - """ - - indexes = range(1, 16) - - # Packet types - CONNECT, CONNACK, PUBLISH, PUBACK, PUBREC, PUBREL, \ - PUBCOMP, SUBSCRIBE, SUBACK, UNSUBSCRIBE, UNSUBACK, \ - PINGREQ, PINGRESP, DISCONNECT, AUTH = indexes - - # Dummy packet type for properties use - will delay only applies to will - WILLMESSAGE = 99 - - Names = ( "reserved", \ - "Connect", "Connack", "Publish", "Puback", "Pubrec", "Pubrel", \ - "Pubcomp", "Subscribe", "Suback", "Unsubscribe", "Unsuback", \ - "Pingreq", "Pingresp", "Disconnect", "Auth") diff --git a/sbapp/mqtt/properties.py b/sbapp/mqtt/properties.py deleted file mode 100644 index f307b86..0000000 --- a/sbapp/mqtt/properties.py +++ /dev/null @@ -1,421 +0,0 @@ -# ******************************************************************* -# Copyright (c) 2017, 2019 IBM Corp. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Eclipse Public License v2.0 -# and Eclipse Distribution License v1.0 which accompany this distribution. -# -# The Eclipse Public License is available at -# http://www.eclipse.org/legal/epl-v20.html -# and the Eclipse Distribution License is available at -# http://www.eclipse.org/org/documents/edl-v10.php. -# -# Contributors: -# Ian Craggs - initial implementation and/or documentation -# ******************************************************************* - -import struct - -from .packettypes import PacketTypes - - -class MQTTException(Exception): - pass - - -class MalformedPacket(MQTTException): - pass - - -def writeInt16(length): - # serialize a 16 bit integer to network format - return bytearray(struct.pack("!H", length)) - - -def readInt16(buf): - # deserialize a 16 bit integer from network format - return struct.unpack("!H", buf[:2])[0] - - -def writeInt32(length): - # serialize a 32 bit integer to network format - return bytearray(struct.pack("!L", length)) - - -def readInt32(buf): - # deserialize a 32 bit integer from network format - return struct.unpack("!L", buf[:4])[0] - - -def writeUTF(data): - # data could be a string, or bytes. If string, encode into bytes with utf-8 - if not isinstance(data, bytes): - data = bytes(data, "utf-8") - return writeInt16(len(data)) + data - - -def readUTF(buffer, maxlen): - if maxlen >= 2: - length = readInt16(buffer) - else: - raise MalformedPacket("Not enough data to read string length") - maxlen -= 2 - if length > maxlen: - raise MalformedPacket("Length delimited string too long") - buf = buffer[2:2+length].decode("utf-8") - # look for chars which are invalid for MQTT - for c in buf: # look for D800-DFFF in the UTF string - ord_c = ord(c) - if ord_c >= 0xD800 and ord_c <= 0xDFFF: - raise MalformedPacket("[MQTT-1.5.4-1] D800-DFFF found in UTF-8 data") - if ord_c == 0x00: # look for null in the UTF string - raise MalformedPacket("[MQTT-1.5.4-2] Null found in UTF-8 data") - if ord_c == 0xFEFF: - raise MalformedPacket("[MQTT-1.5.4-3] U+FEFF in UTF-8 data") - return buf, length+2 - - -def writeBytes(buffer): - return writeInt16(len(buffer)) + buffer - - -def readBytes(buffer): - length = readInt16(buffer) - return buffer[2:2+length], length+2 - - -class VariableByteIntegers: # Variable Byte Integer - """ - MQTT variable byte integer helper class. Used - in several places in MQTT v5.0 properties. - - """ - - @staticmethod - def encode(x): - """ - Convert an integer 0 <= x <= 268435455 into multi-byte format. - Returns the buffer converted from the integer. - """ - if not 0 <= x <= 268435455: - raise ValueError(f"Value {x!r} must be in range 0-268435455") - buffer = b'' - while 1: - digit = x % 128 - x //= 128 - if x > 0: - digit |= 0x80 - buffer += bytes([digit]) - if x == 0: - break - return buffer - - @staticmethod - def decode(buffer): - """ - Get the value of a multi-byte integer from a buffer - Return the value, and the number of bytes used. - - [MQTT-1.5.5-1] the encoded value MUST use the minimum number of bytes necessary to represent the value - """ - multiplier = 1 - value = 0 - bytes = 0 - while 1: - bytes += 1 - digit = buffer[0] - buffer = buffer[1:] - value += (digit & 127) * multiplier - if digit & 128 == 0: - break - multiplier *= 128 - return (value, bytes) - - -class Properties: - """MQTT v5.0 properties class. - - See Properties.names for a list of accepted property names along with their numeric values. - - See Properties.properties for the data type of each property. - - Example of use:: - - publish_properties = Properties(PacketTypes.PUBLISH) - publish_properties.UserProperty = ("a", "2") - publish_properties.UserProperty = ("c", "3") - - First the object is created with packet type as argument, no properties will be present at - this point. Then properties are added as attributes, the name of which is the string property - name without the spaces. - - """ - - def __init__(self, packetType): - self.packetType = packetType - self.types = ["Byte", "Two Byte Integer", "Four Byte Integer", "Variable Byte Integer", - "Binary Data", "UTF-8 Encoded String", "UTF-8 String Pair"] - - self.names = { - "Payload Format Indicator": 1, - "Message Expiry Interval": 2, - "Content Type": 3, - "Response Topic": 8, - "Correlation Data": 9, - "Subscription Identifier": 11, - "Session Expiry Interval": 17, - "Assigned Client Identifier": 18, - "Server Keep Alive": 19, - "Authentication Method": 21, - "Authentication Data": 22, - "Request Problem Information": 23, - "Will Delay Interval": 24, - "Request Response Information": 25, - "Response Information": 26, - "Server Reference": 28, - "Reason String": 31, - "Receive Maximum": 33, - "Topic Alias Maximum": 34, - "Topic Alias": 35, - "Maximum QoS": 36, - "Retain Available": 37, - "User Property": 38, - "Maximum Packet Size": 39, - "Wildcard Subscription Available": 40, - "Subscription Identifier Available": 41, - "Shared Subscription Available": 42 - } - - self.properties = { - # id: type, packets - # payload format indicator - 1: (self.types.index("Byte"), [PacketTypes.PUBLISH, PacketTypes.WILLMESSAGE]), - 2: (self.types.index("Four Byte Integer"), [PacketTypes.PUBLISH, PacketTypes.WILLMESSAGE]), - 3: (self.types.index("UTF-8 Encoded String"), [PacketTypes.PUBLISH, PacketTypes.WILLMESSAGE]), - 8: (self.types.index("UTF-8 Encoded String"), [PacketTypes.PUBLISH, PacketTypes.WILLMESSAGE]), - 9: (self.types.index("Binary Data"), [PacketTypes.PUBLISH, PacketTypes.WILLMESSAGE]), - 11: (self.types.index("Variable Byte Integer"), - [PacketTypes.PUBLISH, PacketTypes.SUBSCRIBE]), - 17: (self.types.index("Four Byte Integer"), - [PacketTypes.CONNECT, PacketTypes.CONNACK, PacketTypes.DISCONNECT]), - 18: (self.types.index("UTF-8 Encoded String"), [PacketTypes.CONNACK]), - 19: (self.types.index("Two Byte Integer"), [PacketTypes.CONNACK]), - 21: (self.types.index("UTF-8 Encoded String"), - [PacketTypes.CONNECT, PacketTypes.CONNACK, PacketTypes.AUTH]), - 22: (self.types.index("Binary Data"), - [PacketTypes.CONNECT, PacketTypes.CONNACK, PacketTypes.AUTH]), - 23: (self.types.index("Byte"), - [PacketTypes.CONNECT]), - 24: (self.types.index("Four Byte Integer"), [PacketTypes.WILLMESSAGE]), - 25: (self.types.index("Byte"), [PacketTypes.CONNECT]), - 26: (self.types.index("UTF-8 Encoded String"), [PacketTypes.CONNACK]), - 28: (self.types.index("UTF-8 Encoded String"), - [PacketTypes.CONNACK, PacketTypes.DISCONNECT]), - 31: (self.types.index("UTF-8 Encoded String"), - [PacketTypes.CONNACK, PacketTypes.PUBACK, PacketTypes.PUBREC, - PacketTypes.PUBREL, PacketTypes.PUBCOMP, PacketTypes.SUBACK, - PacketTypes.UNSUBACK, PacketTypes.DISCONNECT, PacketTypes.AUTH]), - 33: (self.types.index("Two Byte Integer"), - [PacketTypes.CONNECT, PacketTypes.CONNACK]), - 34: (self.types.index("Two Byte Integer"), - [PacketTypes.CONNECT, PacketTypes.CONNACK]), - 35: (self.types.index("Two Byte Integer"), [PacketTypes.PUBLISH]), - 36: (self.types.index("Byte"), [PacketTypes.CONNACK]), - 37: (self.types.index("Byte"), [PacketTypes.CONNACK]), - 38: (self.types.index("UTF-8 String Pair"), - [PacketTypes.CONNECT, PacketTypes.CONNACK, - PacketTypes.PUBLISH, PacketTypes.PUBACK, - PacketTypes.PUBREC, PacketTypes.PUBREL, PacketTypes.PUBCOMP, - PacketTypes.SUBSCRIBE, PacketTypes.SUBACK, - PacketTypes.UNSUBSCRIBE, PacketTypes.UNSUBACK, - PacketTypes.DISCONNECT, PacketTypes.AUTH, PacketTypes.WILLMESSAGE]), - 39: (self.types.index("Four Byte Integer"), - [PacketTypes.CONNECT, PacketTypes.CONNACK]), - 40: (self.types.index("Byte"), [PacketTypes.CONNACK]), - 41: (self.types.index("Byte"), [PacketTypes.CONNACK]), - 42: (self.types.index("Byte"), [PacketTypes.CONNACK]), - } - - def allowsMultiple(self, compressedName): - return self.getIdentFromName(compressedName) in [11, 38] - - def getIdentFromName(self, compressedName): - # return the identifier corresponding to the property name - result = -1 - for name in self.names.keys(): - if compressedName == name.replace(' ', ''): - result = self.names[name] - break - return result - - def __setattr__(self, name, value): - name = name.replace(' ', '') - privateVars = ["packetType", "types", "names", "properties"] - if name in privateVars: - object.__setattr__(self, name, value) - else: - # the name could have spaces in, or not. Remove spaces before assignment - if name not in [aname.replace(' ', '') for aname in self.names.keys()]: - raise MQTTException( - f"Property name must be one of {self.names.keys()}") - # check that this attribute applies to the packet type - if self.packetType not in self.properties[self.getIdentFromName(name)][1]: - raise MQTTException(f"Property {name} does not apply to packet type {PacketTypes.Names[self.packetType]}") - - # Check for forbidden values - if not isinstance(value, list): - if name in ["ReceiveMaximum", "TopicAlias"] \ - and (value < 1 or value > 65535): - - raise MQTTException(f"{name} property value must be in the range 1-65535") - elif name in ["TopicAliasMaximum"] \ - and (value < 0 or value > 65535): - - raise MQTTException(f"{name} property value must be in the range 0-65535") - elif name in ["MaximumPacketSize", "SubscriptionIdentifier"] \ - and (value < 1 or value > 268435455): - - raise MQTTException(f"{name} property value must be in the range 1-268435455") - elif name in ["RequestResponseInformation", "RequestProblemInformation", "PayloadFormatIndicator"] \ - and (value != 0 and value != 1): - - raise MQTTException( - f"{name} property value must be 0 or 1") - - if self.allowsMultiple(name): - if not isinstance(value, list): - value = [value] - if hasattr(self, name): - value = object.__getattribute__(self, name) + value - object.__setattr__(self, name, value) - - def __str__(self): - buffer = "[" - first = True - for name in self.names.keys(): - compressedName = name.replace(' ', '') - if hasattr(self, compressedName): - if not first: - buffer += ", " - buffer += f"{compressedName} : {getattr(self, compressedName)}" - first = False - buffer += "]" - return buffer - - def json(self): - data = {} - for name in self.names.keys(): - compressedName = name.replace(' ', '') - if hasattr(self, compressedName): - val = getattr(self, compressedName) - if compressedName == 'CorrelationData' and isinstance(val, bytes): - data[compressedName] = val.hex() - else: - data[compressedName] = val - return data - - def isEmpty(self): - rc = True - for name in self.names.keys(): - compressedName = name.replace(' ', '') - if hasattr(self, compressedName): - rc = False - break - return rc - - def clear(self): - for name in self.names.keys(): - compressedName = name.replace(' ', '') - if hasattr(self, compressedName): - delattr(self, compressedName) - - def writeProperty(self, identifier, type, value): - buffer = b"" - buffer += VariableByteIntegers.encode(identifier) # identifier - if type == self.types.index("Byte"): # value - buffer += bytes([value]) - elif type == self.types.index("Two Byte Integer"): - buffer += writeInt16(value) - elif type == self.types.index("Four Byte Integer"): - buffer += writeInt32(value) - elif type == self.types.index("Variable Byte Integer"): - buffer += VariableByteIntegers.encode(value) - elif type == self.types.index("Binary Data"): - buffer += writeBytes(value) - elif type == self.types.index("UTF-8 Encoded String"): - buffer += writeUTF(value) - elif type == self.types.index("UTF-8 String Pair"): - buffer += writeUTF(value[0]) + writeUTF(value[1]) - return buffer - - def pack(self): - # serialize properties into buffer for sending over network - buffer = b"" - for name in self.names.keys(): - compressedName = name.replace(' ', '') - if hasattr(self, compressedName): - identifier = self.getIdentFromName(compressedName) - attr_type = self.properties[identifier][0] - if self.allowsMultiple(compressedName): - for prop in getattr(self, compressedName): - buffer += self.writeProperty(identifier, - attr_type, prop) - else: - buffer += self.writeProperty(identifier, attr_type, - getattr(self, compressedName)) - return VariableByteIntegers.encode(len(buffer)) + buffer - - def readProperty(self, buffer, type, propslen): - if type == self.types.index("Byte"): - value = buffer[0] - valuelen = 1 - elif type == self.types.index("Two Byte Integer"): - value = readInt16(buffer) - valuelen = 2 - elif type == self.types.index("Four Byte Integer"): - value = readInt32(buffer) - valuelen = 4 - elif type == self.types.index("Variable Byte Integer"): - value, valuelen = VariableByteIntegers.decode(buffer) - elif type == self.types.index("Binary Data"): - value, valuelen = readBytes(buffer) - elif type == self.types.index("UTF-8 Encoded String"): - value, valuelen = readUTF(buffer, propslen) - elif type == self.types.index("UTF-8 String Pair"): - value, valuelen = readUTF(buffer, propslen) - buffer = buffer[valuelen:] # strip the bytes used by the value - value1, valuelen1 = readUTF(buffer, propslen - valuelen) - value = (value, value1) - valuelen += valuelen1 - return value, valuelen - - def getNameFromIdent(self, identifier): - rc = None - for name in self.names: - if self.names[name] == identifier: - rc = name - return rc - - def unpack(self, buffer): - self.clear() - # deserialize properties into attributes from buffer received from network - propslen, VBIlen = VariableByteIntegers.decode(buffer) - buffer = buffer[VBIlen:] # strip the bytes used by the VBI - propslenleft = propslen - while propslenleft > 0: # properties length is 0 if there are none - identifier, VBIlen2 = VariableByteIntegers.decode( - buffer) # property identifier - buffer = buffer[VBIlen2:] # strip the bytes used by the VBI - propslenleft -= VBIlen2 - attr_type = self.properties[identifier][0] - value, valuelen = self.readProperty( - buffer, attr_type, propslenleft) - buffer = buffer[valuelen:] # strip the bytes used by the value - propslenleft -= valuelen - propname = self.getNameFromIdent(identifier) - compressedName = propname.replace(' ', '') - if not self.allowsMultiple(compressedName) and hasattr(self, compressedName): - raise MQTTException( - f"Property '{property}' must not exist more than once") - setattr(self, propname, value) - return self, propslen + VBIlen diff --git a/sbapp/mqtt/publish.py b/sbapp/mqtt/publish.py deleted file mode 100644 index 333c190..0000000 --- a/sbapp/mqtt/publish.py +++ /dev/null @@ -1,306 +0,0 @@ -# Copyright (c) 2014 Roger Light -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Eclipse Public License v2.0 -# and Eclipse Distribution License v1.0 which accompany this distribution. -# -# The Eclipse Public License is available at -# http://www.eclipse.org/legal/epl-v20.html -# and the Eclipse Distribution License is available at -# http://www.eclipse.org/org/documents/edl-v10.php. -# -# Contributors: -# Roger Light - initial API and implementation - -""" -This module provides some helper functions to allow straightforward publishing -of messages in a one-shot manner. In other words, they are useful for the -situation where you have a single/multiple messages you want to publish to a -broker, then disconnect and nothing else is required. -""" -from __future__ import annotations - -import collections -from collections.abc import Iterable -from typing import TYPE_CHECKING, Any, List, Tuple, Union - -from paho.mqtt.enums import CallbackAPIVersion, MQTTProtocolVersion -from paho.mqtt.properties import Properties -from paho.mqtt.reasoncodes import ReasonCode - -from .. import mqtt -from . import client as paho - -if TYPE_CHECKING: - try: - from typing import NotRequired, Required, TypedDict # type: ignore - except ImportError: - from typing_extensions import NotRequired, Required, TypedDict - - try: - from typing import Literal - except ImportError: - from typing_extensions import Literal # type: ignore - - - - class AuthParameter(TypedDict, total=False): - username: Required[str] - password: NotRequired[str] - - - class TLSParameter(TypedDict, total=False): - ca_certs: Required[str] - certfile: NotRequired[str] - keyfile: NotRequired[str] - tls_version: NotRequired[int] - ciphers: NotRequired[str] - insecure: NotRequired[bool] - - - class MessageDict(TypedDict, total=False): - topic: Required[str] - payload: NotRequired[paho.PayloadType] - qos: NotRequired[int] - retain: NotRequired[bool] - - MessageTuple = Tuple[str, paho.PayloadType, int, bool] - - MessagesList = List[Union[MessageDict, MessageTuple]] - - -def _do_publish(client: paho.Client): - """Internal function""" - - message = client._userdata.popleft() - - if isinstance(message, dict): - client.publish(**message) - elif isinstance(message, (tuple, list)): - client.publish(*message) - else: - raise TypeError('message must be a dict, tuple, or list') - - -def _on_connect(client: paho.Client, userdata: MessagesList, flags, reason_code, properties): - """Internal v5 callback""" - if reason_code == 0: - if len(userdata) > 0: - _do_publish(client) - else: - raise mqtt.MQTTException(paho.connack_string(reason_code)) - - -def _on_publish( - client: paho.Client, userdata: collections.deque[MessagesList], mid: int, reason_codes: ReasonCode, properties: Properties, -) -> None: - """Internal callback""" - #pylint: disable=unused-argument - - if len(userdata) == 0: - client.disconnect() - else: - _do_publish(client) - - -def multiple( - msgs: MessagesList, - hostname: str = "localhost", - port: int = 1883, - client_id: str = "", - keepalive: int = 60, - will: MessageDict | None = None, - auth: AuthParameter | None = None, - tls: TLSParameter | None = None, - protocol: MQTTProtocolVersion = paho.MQTTv311, - transport: Literal["tcp", "websockets"] = "tcp", - proxy_args: Any | None = None, -) -> None: - """Publish multiple messages to a broker, then disconnect cleanly. - - This function creates an MQTT client, connects to a broker and publishes a - list of messages. Once the messages have been delivered, it disconnects - cleanly from the broker. - - :param msgs: a list of messages to publish. Each message is either a dict or a - tuple. - - If a dict, only the topic must be present. Default values will be - used for any missing arguments. The dict must be of the form: - - msg = {'topic':"", 'payload':"", 'qos':, - 'retain':} - topic must be present and may not be empty. - If payload is "", None or not present then a zero length payload - will be published. - If qos is not present, the default of 0 is used. - If retain is not present, the default of False is used. - - If a tuple, then it must be of the form: - ("", "", qos, retain) - - :param str hostname: the address of the broker to connect to. - Defaults to localhost. - - :param int port: the port to connect to the broker on. Defaults to 1883. - - :param str client_id: the MQTT client id to use. If "" or None, the Paho library will - generate a client id automatically. - - :param int keepalive: the keepalive timeout value for the client. Defaults to 60 - seconds. - - :param will: a dict containing will parameters for the client: will = {'topic': - "", 'payload':", 'qos':, 'retain':}. - Topic is required, all other parameters are optional and will - default to None, 0 and False respectively. - Defaults to None, which indicates no will should be used. - - :param auth: a dict containing authentication parameters for the client: - auth = {'username':"", 'password':""} - Username is required, password is optional and will default to None - if not provided. - Defaults to None, which indicates no authentication is to be used. - - :param tls: a dict containing TLS configuration parameters for the client: - dict = {'ca_certs':"", 'certfile':"", - 'keyfile':"", 'tls_version':"", - 'ciphers':", 'insecure':""} - ca_certs is required, all other parameters are optional and will - default to None if not provided, which results in the client using - the default behaviour - see the paho.mqtt.client documentation. - Alternatively, tls input can be an SSLContext object, which will be - processed using the tls_set_context method. - Defaults to None, which indicates that TLS should not be used. - - :param str transport: set to "tcp" to use the default setting of transport which is - raw TCP. Set to "websockets" to use WebSockets as the transport. - - :param proxy_args: a dictionary that will be given to the client. - """ - - if not isinstance(msgs, Iterable): - raise TypeError('msgs must be an iterable') - if len(msgs) == 0: - raise ValueError('msgs is empty') - - client = paho.Client( - CallbackAPIVersion.VERSION2, - client_id=client_id, - userdata=collections.deque(msgs), - protocol=protocol, - transport=transport, - ) - - client.enable_logger() - client.on_publish = _on_publish - client.on_connect = _on_connect # type: ignore - - if proxy_args is not None: - client.proxy_set(**proxy_args) - - if auth: - username = auth.get('username') - if username: - password = auth.get('password') - client.username_pw_set(username, password) - else: - raise KeyError("The 'username' key was not found, this is " - "required for auth") - - if will is not None: - client.will_set(**will) - - if tls is not None: - if isinstance(tls, dict): - insecure = tls.pop('insecure', False) - # mypy don't get that tls no longer contains the key insecure - client.tls_set(**tls) # type: ignore[misc] - if insecure: - # Must be set *after* the `client.tls_set()` call since it sets - # up the SSL context that `client.tls_insecure_set` alters. - client.tls_insecure_set(insecure) - else: - # Assume input is SSLContext object - client.tls_set_context(tls) - - client.connect(hostname, port, keepalive) - client.loop_forever() - - -def single( - topic: str, - payload: paho.PayloadType = None, - qos: int = 0, - retain: bool = False, - hostname: str = "localhost", - port: int = 1883, - client_id: str = "", - keepalive: int = 60, - will: MessageDict | None = None, - auth: AuthParameter | None = None, - tls: TLSParameter | None = None, - protocol: MQTTProtocolVersion = paho.MQTTv311, - transport: Literal["tcp", "websockets"] = "tcp", - proxy_args: Any | None = None, -) -> None: - """Publish a single message to a broker, then disconnect cleanly. - - This function creates an MQTT client, connects to a broker and publishes a - single message. Once the message has been delivered, it disconnects cleanly - from the broker. - - :param str topic: the only required argument must be the topic string to which the - payload will be published. - - :param payload: the payload to be published. If "" or None, a zero length payload - will be published. - - :param int qos: the qos to use when publishing, default to 0. - - :param bool retain: set the message to be retained (True) or not (False). - - :param str hostname: the address of the broker to connect to. - Defaults to localhost. - - :param int port: the port to connect to the broker on. Defaults to 1883. - - :param str client_id: the MQTT client id to use. If "" or None, the Paho library will - generate a client id automatically. - - :param int keepalive: the keepalive timeout value for the client. Defaults to 60 - seconds. - - :param will: a dict containing will parameters for the client: will = {'topic': - "", 'payload':", 'qos':, 'retain':}. - Topic is required, all other parameters are optional and will - default to None, 0 and False respectively. - Defaults to None, which indicates no will should be used. - - :param auth: a dict containing authentication parameters for the client: - Username is required, password is optional and will default to None - auth = {'username':"", 'password':""} - if not provided. - Defaults to None, which indicates no authentication is to be used. - - :param tls: a dict containing TLS configuration parameters for the client: - dict = {'ca_certs':"", 'certfile':"", - 'keyfile':"", 'tls_version':"", - 'ciphers':", 'insecure':""} - ca_certs is required, all other parameters are optional and will - default to None if not provided, which results in the client using - the default behaviour - see the paho.mqtt.client documentation. - Defaults to None, which indicates that TLS should not be used. - Alternatively, tls input can be an SSLContext object, which will be - processed using the tls_set_context method. - - :param transport: set to "tcp" to use the default setting of transport which is - raw TCP. Set to "websockets" to use WebSockets as the transport. - - :param proxy_args: a dictionary that will be given to the client. - """ - - msg: MessageDict = {'topic':topic, 'payload':payload, 'qos':qos, 'retain':retain} - - multiple([msg], hostname, port, client_id, keepalive, will, auth, tls, - protocol, transport, proxy_args) diff --git a/sbapp/mqtt/py.typed b/sbapp/mqtt/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/sbapp/mqtt/reasoncodes.py b/sbapp/mqtt/reasoncodes.py deleted file mode 100644 index 243ac96..0000000 --- a/sbapp/mqtt/reasoncodes.py +++ /dev/null @@ -1,223 +0,0 @@ -# ******************************************************************* -# Copyright (c) 2017, 2019 IBM Corp. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Eclipse Public License v2.0 -# and Eclipse Distribution License v1.0 which accompany this distribution. -# -# The Eclipse Public License is available at -# http://www.eclipse.org/legal/epl-v20.html -# and the Eclipse Distribution License is available at -# http://www.eclipse.org/org/documents/edl-v10.php. -# -# Contributors: -# Ian Craggs - initial implementation and/or documentation -# ******************************************************************* - -import functools -import warnings -from typing import Any - -from .packettypes import PacketTypes - - -@functools.total_ordering -class ReasonCode: - """MQTT version 5.0 reason codes class. - - See ReasonCode.names for a list of possible numeric values along with their - names and the packets to which they apply. - - """ - - def __init__(self, packetType: int, aName: str ="Success", identifier: int =-1): - """ - packetType: the type of the packet, such as PacketTypes.CONNECT that - this reason code will be used with. Some reason codes have different - names for the same identifier when used a different packet type. - - aName: the String name of the reason code to be created. Ignored - if the identifier is set. - - identifier: an integer value of the reason code to be created. - - """ - - self.packetType = packetType - self.names = { - 0: {"Success": [PacketTypes.CONNACK, PacketTypes.PUBACK, - PacketTypes.PUBREC, PacketTypes.PUBREL, PacketTypes.PUBCOMP, - PacketTypes.UNSUBACK, PacketTypes.AUTH], - "Normal disconnection": [PacketTypes.DISCONNECT], - "Granted QoS 0": [PacketTypes.SUBACK]}, - 1: {"Granted QoS 1": [PacketTypes.SUBACK]}, - 2: {"Granted QoS 2": [PacketTypes.SUBACK]}, - 4: {"Disconnect with will message": [PacketTypes.DISCONNECT]}, - 16: {"No matching subscribers": - [PacketTypes.PUBACK, PacketTypes.PUBREC]}, - 17: {"No subscription found": [PacketTypes.UNSUBACK]}, - 24: {"Continue authentication": [PacketTypes.AUTH]}, - 25: {"Re-authenticate": [PacketTypes.AUTH]}, - 128: {"Unspecified error": [PacketTypes.CONNACK, PacketTypes.PUBACK, - PacketTypes.PUBREC, PacketTypes.SUBACK, PacketTypes.UNSUBACK, - PacketTypes.DISCONNECT], }, - 129: {"Malformed packet": - [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, - 130: {"Protocol error": - [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, - 131: {"Implementation specific error": [PacketTypes.CONNACK, - PacketTypes.PUBACK, PacketTypes.PUBREC, PacketTypes.SUBACK, - PacketTypes.UNSUBACK, PacketTypes.DISCONNECT], }, - 132: {"Unsupported protocol version": [PacketTypes.CONNACK]}, - 133: {"Client identifier not valid": [PacketTypes.CONNACK]}, - 134: {"Bad user name or password": [PacketTypes.CONNACK]}, - 135: {"Not authorized": [PacketTypes.CONNACK, PacketTypes.PUBACK, - PacketTypes.PUBREC, PacketTypes.SUBACK, PacketTypes.UNSUBACK, - PacketTypes.DISCONNECT], }, - 136: {"Server unavailable": [PacketTypes.CONNACK]}, - 137: {"Server busy": [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, - 138: {"Banned": [PacketTypes.CONNACK]}, - 139: {"Server shutting down": [PacketTypes.DISCONNECT]}, - 140: {"Bad authentication method": - [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, - 141: {"Keep alive timeout": [PacketTypes.DISCONNECT]}, - 142: {"Session taken over": [PacketTypes.DISCONNECT]}, - 143: {"Topic filter invalid": - [PacketTypes.SUBACK, PacketTypes.UNSUBACK, PacketTypes.DISCONNECT]}, - 144: {"Topic name invalid": - [PacketTypes.CONNACK, PacketTypes.PUBACK, - PacketTypes.PUBREC, PacketTypes.DISCONNECT]}, - 145: {"Packet identifier in use": - [PacketTypes.PUBACK, PacketTypes.PUBREC, - PacketTypes.SUBACK, PacketTypes.UNSUBACK]}, - 146: {"Packet identifier not found": - [PacketTypes.PUBREL, PacketTypes.PUBCOMP]}, - 147: {"Receive maximum exceeded": [PacketTypes.DISCONNECT]}, - 148: {"Topic alias invalid": [PacketTypes.DISCONNECT]}, - 149: {"Packet too large": [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, - 150: {"Message rate too high": [PacketTypes.DISCONNECT]}, - 151: {"Quota exceeded": [PacketTypes.CONNACK, PacketTypes.PUBACK, - PacketTypes.PUBREC, PacketTypes.SUBACK, PacketTypes.DISCONNECT], }, - 152: {"Administrative action": [PacketTypes.DISCONNECT]}, - 153: {"Payload format invalid": - [PacketTypes.PUBACK, PacketTypes.PUBREC, PacketTypes.DISCONNECT]}, - 154: {"Retain not supported": - [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, - 155: {"QoS not supported": - [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, - 156: {"Use another server": - [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, - 157: {"Server moved": - [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, - 158: {"Shared subscription not supported": - [PacketTypes.SUBACK, PacketTypes.DISCONNECT]}, - 159: {"Connection rate exceeded": - [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, - 160: {"Maximum connect time": - [PacketTypes.DISCONNECT]}, - 161: {"Subscription identifiers not supported": - [PacketTypes.SUBACK, PacketTypes.DISCONNECT]}, - 162: {"Wildcard subscription not supported": - [PacketTypes.SUBACK, PacketTypes.DISCONNECT]}, - } - if identifier == -1: - if packetType == PacketTypes.DISCONNECT and aName == "Success": - aName = "Normal disconnection" - self.set(aName) - else: - self.value = identifier - self.getName() # check it's good - - def __getName__(self, packetType, identifier): - """ - Get the reason code string name for a specific identifier. - The name can vary by packet type for the same identifier, which - is why the packet type is also required. - - Used when displaying the reason code. - """ - if identifier not in self.names: - raise KeyError(identifier) - names = self.names[identifier] - namelist = [name for name in names.keys() if packetType in names[name]] - if len(namelist) != 1: - raise ValueError(f"Expected exactly one name, found {namelist!r}") - return namelist[0] - - def getId(self, name): - """ - Get the numeric id corresponding to a reason code name. - - Used when setting the reason code for a packetType - check that only valid codes for the packet are set. - """ - for code in self.names.keys(): - if name in self.names[code].keys(): - if self.packetType in self.names[code][name]: - return code - raise KeyError(f"Reason code name not found: {name}") - - def set(self, name): - self.value = self.getId(name) - - def unpack(self, buffer): - c = buffer[0] - name = self.__getName__(self.packetType, c) - self.value = self.getId(name) - return 1 - - def getName(self): - """Returns the reason code name corresponding to the numeric value which is set. - """ - return self.__getName__(self.packetType, self.value) - - def __eq__(self, other): - if isinstance(other, int): - return self.value == other - if isinstance(other, str): - return other == str(self) - if isinstance(other, ReasonCode): - return self.value == other.value - return False - - def __lt__(self, other): - if isinstance(other, int): - return self.value < other - if isinstance(other, ReasonCode): - return self.value < other.value - return NotImplemented - - def __repr__(self): - try: - packet_name = PacketTypes.Names[self.packetType] - except IndexError: - packet_name = "Unknown" - - return f"ReasonCode({packet_name}, {self.getName()!r})" - - def __str__(self): - return self.getName() - - def json(self): - return self.getName() - - def pack(self): - return bytearray([self.value]) - - @property - def is_failure(self) -> bool: - return self.value >= 0x80 - - -class _CompatibilityIsInstance(type): - def __instancecheck__(self, other: Any) -> bool: - return isinstance(other, ReasonCode) - - -class ReasonCodes(ReasonCode, metaclass=_CompatibilityIsInstance): - def __init__(self, *args, **kwargs): - warnings.warn("ReasonCodes is deprecated, use ReasonCode (singular) instead", - category=DeprecationWarning, - stacklevel=2, - ) - super().__init__(*args, **kwargs) diff --git a/sbapp/mqtt/subscribe.py b/sbapp/mqtt/subscribe.py deleted file mode 100644 index b6c80f4..0000000 --- a/sbapp/mqtt/subscribe.py +++ /dev/null @@ -1,281 +0,0 @@ -# Copyright (c) 2016 Roger Light -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Eclipse Public License v2.0 -# and Eclipse Distribution License v1.0 which accompany this distribution. -# -# The Eclipse Public License is available at -# http://www.eclipse.org/legal/epl-v20.html -# and the Eclipse Distribution License is available at -# http://www.eclipse.org/org/documents/edl-v10.php. -# -# Contributors: -# Roger Light - initial API and implementation - -""" -This module provides some helper functions to allow straightforward subscribing -to topics and retrieving messages. The two functions are simple(), which -returns one or messages matching a set of topics, and callback() which allows -you to pass a callback for processing of messages. -""" - -from .. import mqtt -from . import client as paho - - -def _on_connect(client, userdata, flags, reason_code, properties): - """Internal callback""" - if reason_code != 0: - raise mqtt.MQTTException(paho.connack_string(reason_code)) - - if isinstance(userdata['topics'], list): - for topic in userdata['topics']: - client.subscribe(topic, userdata['qos']) - else: - client.subscribe(userdata['topics'], userdata['qos']) - - -def _on_message_callback(client, userdata, message): - """Internal callback""" - userdata['callback'](client, userdata['userdata'], message) - - -def _on_message_simple(client, userdata, message): - """Internal callback""" - - if userdata['msg_count'] == 0: - return - - # Don't process stale retained messages if 'retained' was false - if message.retain and not userdata['retained']: - return - - userdata['msg_count'] = userdata['msg_count'] - 1 - - if userdata['messages'] is None and userdata['msg_count'] == 0: - userdata['messages'] = message - client.disconnect() - return - - userdata['messages'].append(message) - if userdata['msg_count'] == 0: - client.disconnect() - - -def callback(callback, topics, qos=0, userdata=None, hostname="localhost", - port=1883, client_id="", keepalive=60, will=None, auth=None, - tls=None, protocol=paho.MQTTv311, transport="tcp", - clean_session=True, proxy_args=None): - """Subscribe to a list of topics and process them in a callback function. - - This function creates an MQTT client, connects to a broker and subscribes - to a list of topics. Incoming messages are processed by the user provided - callback. This is a blocking function and will never return. - - :param callback: function with the same signature as `on_message` for - processing the messages received. - - :param topics: either a string containing a single topic to subscribe to, or a - list of topics to subscribe to. - - :param int qos: the qos to use when subscribing. This is applied to all topics. - - :param userdata: passed to the callback - - :param str hostname: the address of the broker to connect to. - Defaults to localhost. - - :param int port: the port to connect to the broker on. Defaults to 1883. - - :param str client_id: the MQTT client id to use. If "" or None, the Paho library will - generate a client id automatically. - - :param int keepalive: the keepalive timeout value for the client. Defaults to 60 - seconds. - - :param will: a dict containing will parameters for the client: will = {'topic': - "", 'payload':", 'qos':, 'retain':}. - Topic is required, all other parameters are optional and will - default to None, 0 and False respectively. - - Defaults to None, which indicates no will should be used. - - :param auth: a dict containing authentication parameters for the client: - auth = {'username':"", 'password':""} - Username is required, password is optional and will default to None - if not provided. - Defaults to None, which indicates no authentication is to be used. - - :param tls: a dict containing TLS configuration parameters for the client: - dict = {'ca_certs':"", 'certfile':"", - 'keyfile':"", 'tls_version':"", - 'ciphers':", 'insecure':""} - ca_certs is required, all other parameters are optional and will - default to None if not provided, which results in the client using - the default behaviour - see the paho.mqtt.client documentation. - Alternatively, tls input can be an SSLContext object, which will be - processed using the tls_set_context method. - Defaults to None, which indicates that TLS should not be used. - - :param str transport: set to "tcp" to use the default setting of transport which is - raw TCP. Set to "websockets" to use WebSockets as the transport. - - :param clean_session: a boolean that determines the client type. If True, - the broker will remove all information about this client - when it disconnects. If False, the client is a persistent - client and subscription information and queued messages - will be retained when the client disconnects. - Defaults to True. - - :param proxy_args: a dictionary that will be given to the client. - """ - - if qos < 0 or qos > 2: - raise ValueError('qos must be in the range 0-2') - - callback_userdata = { - 'callback':callback, - 'topics':topics, - 'qos':qos, - 'userdata':userdata} - - client = paho.Client( - paho.CallbackAPIVersion.VERSION2, - client_id=client_id, - userdata=callback_userdata, - protocol=protocol, - transport=transport, - clean_session=clean_session, - ) - client.enable_logger() - - client.on_message = _on_message_callback - client.on_connect = _on_connect - - if proxy_args is not None: - client.proxy_set(**proxy_args) - - if auth: - username = auth.get('username') - if username: - password = auth.get('password') - client.username_pw_set(username, password) - else: - raise KeyError("The 'username' key was not found, this is " - "required for auth") - - if will is not None: - client.will_set(**will) - - if tls is not None: - if isinstance(tls, dict): - insecure = tls.pop('insecure', False) - client.tls_set(**tls) - if insecure: - # Must be set *after* the `client.tls_set()` call since it sets - # up the SSL context that `client.tls_insecure_set` alters. - client.tls_insecure_set(insecure) - else: - # Assume input is SSLContext object - client.tls_set_context(tls) - - client.connect(hostname, port, keepalive) - client.loop_forever() - - -def simple(topics, qos=0, msg_count=1, retained=True, hostname="localhost", - port=1883, client_id="", keepalive=60, will=None, auth=None, - tls=None, protocol=paho.MQTTv311, transport="tcp", - clean_session=True, proxy_args=None): - """Subscribe to a list of topics and return msg_count messages. - - This function creates an MQTT client, connects to a broker and subscribes - to a list of topics. Once "msg_count" messages have been received, it - disconnects cleanly from the broker and returns the messages. - - :param topics: either a string containing a single topic to subscribe to, or a - list of topics to subscribe to. - - :param int qos: the qos to use when subscribing. This is applied to all topics. - - :param int msg_count: the number of messages to retrieve from the broker. - if msg_count == 1 then a single MQTTMessage will be returned. - if msg_count > 1 then a list of MQTTMessages will be returned. - - :param bool retained: If set to True, retained messages will be processed the same as - non-retained messages. If set to False, retained messages will - be ignored. This means that with retained=False and msg_count=1, - the function will return the first message received that does - not have the retained flag set. - - :param str hostname: the address of the broker to connect to. - Defaults to localhost. - - :param int port: the port to connect to the broker on. Defaults to 1883. - - :param str client_id: the MQTT client id to use. If "" or None, the Paho library will - generate a client id automatically. - - :param int keepalive: the keepalive timeout value for the client. Defaults to 60 - seconds. - - :param will: a dict containing will parameters for the client: will = {'topic': - "", 'payload':", 'qos':, 'retain':}. - Topic is required, all other parameters are optional and will - default to None, 0 and False respectively. - Defaults to None, which indicates no will should be used. - - :param auth: a dict containing authentication parameters for the client: - auth = {'username':"", 'password':""} - Username is required, password is optional and will default to None - if not provided. - Defaults to None, which indicates no authentication is to be used. - - :param tls: a dict containing TLS configuration parameters for the client: - dict = {'ca_certs':"", 'certfile':"", - 'keyfile':"", 'tls_version':"", - 'ciphers':", 'insecure':""} - ca_certs is required, all other parameters are optional and will - default to None if not provided, which results in the client using - the default behaviour - see the paho.mqtt.client documentation. - Alternatively, tls input can be an SSLContext object, which will be - processed using the tls_set_context method. - Defaults to None, which indicates that TLS should not be used. - - :param protocol: the MQTT protocol version to use. Defaults to MQTTv311. - - :param transport: set to "tcp" to use the default setting of transport which is - raw TCP. Set to "websockets" to use WebSockets as the transport. - - :param clean_session: a boolean that determines the client type. If True, - the broker will remove all information about this client - when it disconnects. If False, the client is a persistent - client and subscription information and queued messages - will be retained when the client disconnects. - Defaults to True. If protocol is MQTTv50, clean_session - is ignored. - - :param proxy_args: a dictionary that will be given to the client. - """ - - if msg_count < 1: - raise ValueError('msg_count must be > 0') - - # Set ourselves up to return a single message if msg_count == 1, or a list - # if > 1. - if msg_count == 1: - messages = None - else: - messages = [] - - # Ignore clean_session if protocol is MQTTv50, otherwise Client will raise - if protocol == paho.MQTTv5: - clean_session = None - - userdata = {'retained':retained, 'msg_count':msg_count, 'messages':messages} - - callback(_on_message_simple, topics, qos, userdata, hostname, port, - client_id, keepalive, will, auth, tls, protocol, transport, - clean_session, proxy_args) - - return userdata['messages'] diff --git a/sbapp/mqtt/subscribeoptions.py b/sbapp/mqtt/subscribeoptions.py deleted file mode 100644 index 7e0605d..0000000 --- a/sbapp/mqtt/subscribeoptions.py +++ /dev/null @@ -1,113 +0,0 @@ -""" -******************************************************************* - Copyright (c) 2017, 2019 IBM Corp. - - All rights reserved. This program and the accompanying materials - are made available under the terms of the Eclipse Public License v2.0 - and Eclipse Distribution License v1.0 which accompany this distribution. - - The Eclipse Public License is available at - http://www.eclipse.org/legal/epl-v20.html - and the Eclipse Distribution License is available at - http://www.eclipse.org/org/documents/edl-v10.php. - - Contributors: - Ian Craggs - initial implementation and/or documentation -******************************************************************* -""" - - - -class MQTTException(Exception): - pass - - -class SubscribeOptions: - """The MQTT v5.0 subscribe options class. - - The options are: - qos: As in MQTT v3.1.1. - noLocal: True or False. If set to True, the subscriber will not receive its own publications. - retainAsPublished: True or False. If set to True, the retain flag on received publications will be as set - by the publisher. - retainHandling: RETAIN_SEND_ON_SUBSCRIBE, RETAIN_SEND_IF_NEW_SUB or RETAIN_DO_NOT_SEND - Controls when the broker should send retained messages: - - RETAIN_SEND_ON_SUBSCRIBE: on any successful subscribe request - - RETAIN_SEND_IF_NEW_SUB: only if the subscribe request is new - - RETAIN_DO_NOT_SEND: never send retained messages - """ - - # retain handling options - RETAIN_SEND_ON_SUBSCRIBE, RETAIN_SEND_IF_NEW_SUB, RETAIN_DO_NOT_SEND = range( - 0, 3) - - def __init__( - self, - qos: int = 0, - noLocal: bool = False, - retainAsPublished: bool = False, - retainHandling: int = RETAIN_SEND_ON_SUBSCRIBE, - ): - """ - qos: 0, 1 or 2. 0 is the default. - noLocal: True or False. False is the default and corresponds to MQTT v3.1.1 behavior. - retainAsPublished: True or False. False is the default and corresponds to MQTT v3.1.1 behavior. - retainHandling: RETAIN_SEND_ON_SUBSCRIBE, RETAIN_SEND_IF_NEW_SUB or RETAIN_DO_NOT_SEND - RETAIN_SEND_ON_SUBSCRIBE is the default and corresponds to MQTT v3.1.1 behavior. - """ - object.__setattr__(self, "names", - ["QoS", "noLocal", "retainAsPublished", "retainHandling"]) - self.QoS = qos # bits 0,1 - self.noLocal = noLocal # bit 2 - self.retainAsPublished = retainAsPublished # bit 3 - self.retainHandling = retainHandling # bits 4 and 5: 0, 1 or 2 - if self.retainHandling not in (0, 1, 2): - raise AssertionError(f"Retain handling should be 0, 1 or 2, not {self.retainHandling}") - if self.QoS not in (0, 1, 2): - raise AssertionError(f"QoS should be 0, 1 or 2, not {self.QoS}") - - def __setattr__(self, name, value): - if name not in self.names: - raise MQTTException( - f"{name} Attribute name must be one of {self.names}") - object.__setattr__(self, name, value) - - def pack(self): - if self.retainHandling not in (0, 1, 2): - raise AssertionError(f"Retain handling should be 0, 1 or 2, not {self.retainHandling}") - if self.QoS not in (0, 1, 2): - raise AssertionError(f"QoS should be 0, 1 or 2, not {self.QoS}") - noLocal = 1 if self.noLocal else 0 - retainAsPublished = 1 if self.retainAsPublished else 0 - data = [(self.retainHandling << 4) | (retainAsPublished << 3) | - (noLocal << 2) | self.QoS] - return bytes(data) - - def unpack(self, buffer): - b0 = buffer[0] - self.retainHandling = ((b0 >> 4) & 0x03) - self.retainAsPublished = True if ((b0 >> 3) & 0x01) == 1 else False - self.noLocal = True if ((b0 >> 2) & 0x01) == 1 else False - self.QoS = (b0 & 0x03) - if self.retainHandling not in (0, 1, 2): - raise AssertionError(f"Retain handling should be 0, 1 or 2, not {self.retainHandling}") - if self.QoS not in (0, 1, 2): - raise AssertionError(f"QoS should be 0, 1 or 2, not {self.QoS}") - return 1 - - def __repr__(self): - return str(self) - - def __str__(self): - return "{QoS="+str(self.QoS)+", noLocal="+str(self.noLocal) +\ - ", retainAsPublished="+str(self.retainAsPublished) +\ - ", retainHandling="+str(self.retainHandling)+"}" - - def json(self): - data = { - "QoS": self.QoS, - "noLocal": self.noLocal, - "retainAsPublished": self.retainAsPublished, - "retainHandling": self.retainHandling, - } - return data diff --git a/sbapp/sideband/core.py b/sbapp/sideband/core.py index ce5547d..fa43164 100644 --- a/sbapp/sideband/core.py +++ b/sbapp/sideband/core.py @@ -3228,9 +3228,10 @@ class SidebandCore(): if self.config["telemetry_enabled"] == True: self.update_telemeter_config() if self.telemeter != None: - def mqtt_job(): - self.mqtt_handle_telemetry(self.lxmf_destination.hash, self.telemeter.packed()) - threading.Thread(target=mqtt_job, daemon=True).start() + if self.config["telemetry_to_mqtt"]: + def mqtt_job(): + self.mqtt_handle_telemetry(self.lxmf_destination.hash, self.telemeter.packed()) + threading.Thread(target=mqtt_job, daemon=True).start() return self.telemeter.read_all() else: return {} diff --git a/sbapp/sideband/mqtt.py b/sbapp/sideband/mqtt.py index 0d69816..0d4837e 100644 --- a/sbapp/sideband/mqtt.py +++ b/sbapp/sideband/mqtt.py @@ -2,9 +2,13 @@ import RNS import time import threading from collections import deque -from sbapp.mqtt import client as mqtt from .sense import Telemeter, Commands +if RNS.vendor.platformutils.get_platform() == "android": + import pmqtt.client as mqtt +else: + from sbapp.pmqtt import client as mqtt + class MQTT(): QUEUE_MAXLEN = 65536 SCHEDULER_SLEEP = 1 diff --git a/sbapp/sideband/sense.py b/sbapp/sideband/sense.py index 7716417..1aa5a9b 100644 --- a/sbapp/sideband/sense.py +++ b/sbapp/sideband/sense.py @@ -2542,7 +2542,6 @@ class RNSTransport(Sensor): rss = ifstats.pop("rss") if self.last_update == 0: - RNS.log("NO CALC DIFF") rxs = ifstats["rxs"] txs = ifstats["txs"] else: @@ -2551,9 +2550,6 @@ class RNSTransport(Sensor): txd = ifstats["txb"] - self._last_traffic_txb rxs = (rxd/td)*8 txs = (txd/td)*8 - RNS.log(f"CALC DIFFS: td={td}, rxd={rxd}, txd={txd}") - RNS.log(f" rxs={rxs}, txs={txs}") - self._last_traffic_rxb = ifstats["rxb"] self._last_traffic_txb = ifstats["txb"]