diff --git a/lib/paho/mqtt/__init__.py b/lib/paho/mqtt/__init__.py index 19b6a4ab..377cecc7 100644 --- a/lib/paho/mqtt/__init__.py +++ b/lib/paho/mqtt/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2.0.0" +__version__ = "2.1.0" class MQTTException(Exception): diff --git a/lib/paho/mqtt/client.py b/lib/paho/mqtt/client.py index f5897328..4ccc8696 100644 --- a/lib/paho/mqtt/client.py +++ b/lib/paho/mqtt/client.py @@ -288,7 +288,7 @@ class WebsocketConnectionError(ConnectionError): pass -def error_string(mqtt_errno: MQTTErrorCode) -> str: +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." @@ -465,7 +465,7 @@ def _force_bytes(s: str | bytes) -> bytes: return s -def _encode_payload(payload: str | bytes | bytearray | int | float | None) -> bytes: +def _encode_payload(payload: str | bytes | bytearray | int | float | None) -> bytes|bytearray: if isinstance(payload, str): return payload.encode("utf-8") @@ -648,7 +648,7 @@ class Client: :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 migrations.md contains details on + 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 @@ -682,6 +682,10 @@ class Client: :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 @@ -728,19 +732,21 @@ class Client: def __init__( self, - callback_api_version: CallbackAPIVersion, - client_id: str = "", + callback_api_version: CallbackAPIVersion = CallbackAPIVersion.VERSION1, + client_id: str | None = "", clean_session: bool | None = None, userdata: Any = None, - protocol: int = MQTTv311, - transport: Literal["tcp", "websockets"] = "tcp", + 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 not in ("websockets", "tcp"): + 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" or "tcp", not {transport}') + f'transport must be "websockets", "tcp" or "unix", not {transport}') self._manual_ack = manual_ack self._transport = transport @@ -764,7 +770,7 @@ class Client: # 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 migrations.md for details" + "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") @@ -931,7 +937,7 @@ class Client: self._keepalive = value @property - def transport(self) -> Literal["tcp", "websockets"]: + def transport(self) -> Literal["tcp", "websockets", "unix"]: """ Transport method used for the connection ("tcp" or "websockets"). @@ -953,7 +959,7 @@ class Client: This property is read-only. """ - return self.protocol + return self._protocol @property def connect_timeout(self) -> float: @@ -2032,7 +2038,7 @@ class Client: return self._send_subscribe(False, topic_qos_list, properties) def unsubscribe( - self, topic: str, properties: Properties | None = None + self, topic: str | list[str], properties: Properties | None = None ) -> tuple[MQTTErrorCode, int | None]: """Unsubscribe the client from one or more topics. @@ -2356,7 +2362,6 @@ class Client: self._thread_terminate = True if threading.current_thread() != self._thread: self._thread.join() - self._thread = None return MQTTErrorCode.MQTT_ERR_SUCCESS @@ -2445,7 +2450,7 @@ class Client: connect_callback(client, userdata, flags, rc) - * For MQTT it's v5.0:: + * For MQTT v5.0 it's:: connect_callback(client, userdata, flags, reason_code, properties) @@ -2732,7 +2737,7 @@ class Client: disconnect_callback(client, userdata, rc) - * For MQTT it's v5.0:: + * For MQTT v5.0 it's:: disconnect_callback(client, userdata, reason_code, properties) @@ -3363,7 +3368,7 @@ class Client: self, mid: int, topic: bytes, - payload: bytes = b"", + payload: bytes|bytearray = b"", qos: int = 0, retain: bool = False, dup: bool = False, @@ -3373,7 +3378,7 @@ class Client: # 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): + if payload and not isinstance(payload, (bytes, bytearray)): raise TypeError('payload must be bytes if set') if self._sock is None: @@ -3462,7 +3467,7 @@ class Client: return self._packet_queue(command, packet, 0, 0) def _send_connect(self, keepalive: int) -> MQTTErrorCode: - proto_ver = self._protocol + proto_ver = int(self._protocol) # hard-coded UTF-8 encoded string protocol = b"MQTT" if proto_ver >= MQTTv311 else b"MQIsdp" @@ -4514,7 +4519,10 @@ class Client: MQTT_LOG_ERR, 'Caught exception in on_connect_fail: %s', err) def _thread_main(self) -> None: - self.loop_forever(retry_first_connection=True) + try: + self.loop_forever(retry_first_connection=True) + finally: + self._thread = None def _reconnect_wait(self) -> None: # See reconnect_delay_set for details @@ -4595,7 +4603,11 @@ class Client: return None def _create_socket(self) -> SocketLike: - sock = self._create_socket_connection() + 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) @@ -4612,6 +4624,11 @@ class Client: 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) diff --git a/lib/paho/mqtt/publish.py b/lib/paho/mqtt/publish.py index 42435156..333c190a 100644 --- a/lib/paho/mqtt/publish.py +++ b/lib/paho/mqtt/publish.py @@ -24,7 +24,7 @@ import collections from collections.abc import Iterable from typing import TYPE_CHECKING, Any, List, Tuple, Union -from paho.mqtt.enums import CallbackAPIVersion +from paho.mqtt.enums import CallbackAPIVersion, MQTTProtocolVersion from paho.mqtt.properties import Properties from paho.mqtt.reasoncodes import ReasonCode @@ -112,7 +112,7 @@ def multiple( will: MessageDict | None = None, auth: AuthParameter | None = None, tls: TLSParameter | None = None, - protocol: int = paho.MQTTv311, + protocol: MQTTProtocolVersion = paho.MQTTv311, transport: Literal["tcp", "websockets"] = "tcp", proxy_args: Any | None = None, ) -> None: @@ -240,7 +240,7 @@ def single( will: MessageDict | None = None, auth: AuthParameter | None = None, tls: TLSParameter | None = None, - protocol: int = paho.MQTTv311, + protocol: MQTTProtocolVersion = paho.MQTTv311, transport: Literal["tcp", "websockets"] = "tcp", proxy_args: Any | None = None, ) -> None: diff --git a/lib/paho/mqtt/reasoncodes.py b/lib/paho/mqtt/reasoncodes.py index 6b30cb83..243ac96f 100644 --- a/lib/paho/mqtt/reasoncodes.py +++ b/lib/paho/mqtt/reasoncodes.py @@ -30,7 +30,7 @@ class ReasonCode: """ - def __init__(self, packetType, aName="Success", identifier=-1): + 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 diff --git a/requirements.txt b/requirements.txt index 590f4fc1..f2d316c2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,7 @@ Mako==1.3.3 MarkupSafe==2.1.5 musicbrainzngs==0.7.1 packaging==24.0 -paho-mqtt==2.0.0 +paho-mqtt==2.1.0 platformdirs==4.2.1 plexapi==4.15.12 portend==3.2.0