Repository: empicano/aiomqtt (language: python)

Issue:
Make aiomqtt compatible with paho v2
We could use `paho.mqtt.__version__` to determine at runtime whether to pass `mqtt.CallbackAPIVersion.VERSION1` to the init call to the paho client.
```python
if mqtt.__version__.startswith("2"):
    client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1)
else:
   client = mqtt.Client()
```


Discussion / hints:
Is there a compelling reason to not couple "^2.0" and work with it as it is by default?

I did a quick look at the paho v2 asyncio examples and it doesn't look like much has changed / changes look minimal.
I will do the PR tonight.
This is not as trivial as I thought, the typing and mypy are getting in the way.
Hey @empicano, @frederikaalund,

Managing typings for both paho-mqtt 1.6.1 and the newer 2.0 version adds many complexities. Given that 1.6.1 is significantly outdated now, I'm leaning towards fully embracing version 2.0 and discontinuing support for the older one.

This will allow us to leverage the latest features and improvements in 2.0. What are your thoughts on making this transition?

Fix patch:
diff --git a/aiomqtt/client.py b/aiomqtt/client.py
index 8d64de4..4e52517 100644
--- a/aiomqtt/client.py
+++ b/aiomqtt/client.py
@@ -21,11 +21,16 @@
     Generator,
     Iterable,
     Iterator,
+    Literal,
     TypeVar,
     cast,
 )
 
 import paho.mqtt.client as mqtt
+from paho.mqtt.enums import CallbackAPIVersion
+from paho.mqtt.properties import Properties
+from paho.mqtt.reasoncodes import ReasonCode
+from paho.mqtt.subscribeoptions import SubscribeOptions
 
 from .exceptions import MqttCodeError, MqttConnectError, MqttError, MqttReentrantError
 from .message import Message
@@ -116,7 +121,7 @@ class Will:
     payload: PayloadType | None = None
     qos: int = 0
     retain: bool = False
-    properties: mqtt.Properties | None = None
+    properties: Properties | None = None
 
 
 class Client:
@@ -185,17 +190,17 @@ def __init__(  # noqa: C901, PLR0912, PLR0913, PLR0915
         protocol: ProtocolVersion | None = None,
         will: Will | None = None,
         clean_session: bool | None = None,
-        transport: str = "tcp",
+        transport: Literal["tcp", "websockets"] = "tcp",
         timeout: float | None = None,
         keepalive: int = 60,
         bind_address: str = "",
         bind_port: int = 0,
-        clean_start: int = mqtt.MQTT_CLEAN_START_FIRST_ONLY,
+        clean_start: mqtt.CleanStartOption = mqtt.MQTT_CLEAN_START_FIRST_ONLY,
         max_queued_incoming_messages: int | None = None,
         max_queued_outgoing_messages: int | None = None,
         max_inflight_messages: int | None = None,
         max_concurrent_outgoing_calls: int | None = None,
-        properties: mqtt.Properties | None = None,
+        properties: Properties | None = None,
         tls_context: ssl.SSLContext | None = None,
         tls_params: TLSParameters | None = None,
         tls_insecure: bool | None = None,
@@ -220,7 +225,7 @@ def __init__(  # noqa: C901, PLR0912, PLR0913, PLR0915
 
         # Pending subscribe, unsubscribe, and publish calls
         self._pending_subscribes: dict[
-            int, asyncio.Future[tuple[int] | list[mqtt.ReasonCodes]]
+            int, asyncio.Future[tuple[int, ...] | list[ReasonCode]]
         ] = {}
         self._pending_unsubscribes: dict[int, asyncio.Event] = {}
         self._pending_publishes: dict[int, asyncio.Event] = {}
@@ -247,7 +252,8 @@ def __init__(  # noqa: C901, PLR0912, PLR0913, PLR0915
 
         # Create the underlying paho-mqtt client instance
         self._client: mqtt.Client = mqtt.Client(
-            client_id=identifier,
+            callback_api_version=CallbackAPIVersion.VERSION1,
+            client_id=identifier,  # type: ignore[arg-type]
             protocol=protocol,
             clean_session=clean_session,
             transport=transport,
@@ -322,7 +328,7 @@ def identifier(self) -> str:
         Note that paho-mqtt stores the client ID as `bytes` internally. We assume that
         the client ID is a UTF8-encoded string and decode it first.
         """
-        return cast(bytes, self._client._client_id).decode()  # type: ignore[attr-defined] # noqa: SLF001
+        return self._client._client_id.decode()  # noqa: SLF001
 
     @property
     def _pending_calls(self) -> Generator[int, None, None]:
@@ -337,12 +343,12 @@ async def subscribe(  # noqa: PLR0913
         /,
         topic: SubscribeTopic,
         qos: int = 0,
-        options: mqtt.SubscribeOptions | None = None,
-        properties: mqtt.Properties | None = None,
+        options: SubscribeOptions | None = None,
+        properties: Properties | None = None,
         *args: Any,
         timeout: float | None = None,
         **kwargs: Any,
-    ) -> tuple[int] | list[mqtt.ReasonCodes]:
+    ) -> tuple[int, ...] | list[ReasonCode]:
         """Subscribe to a topic or wildcard.
 
         Args:
@@ -362,11 +368,11 @@ async def subscribe(  # noqa: PLR0913
             topic, qos, options, properties, *args, **kwargs
         )
         # Early out on error
-        if result != mqtt.MQTT_ERR_SUCCESS:
+        if result != mqtt.MQTT_ERR_SUCCESS or mid is None:
             raise MqttCodeError(result, "Could not subscribe to topic")
         # Create future for when the on_subscribe callback is called
         callback_result: asyncio.Future[
-            tuple[int] | list[mqtt.ReasonCodes]
+            tuple[int, ...] | list[ReasonCode]
         ] = asyncio.Future()
         with self._pending_call(mid, callback_result, self._pending_subscribes):
             # Wait for callback_result
@@ -377,7 +383,7 @@ async def unsubscribe(
         self,
         /,
         topic: str | list[str],
-        properties: mqtt.Properties | None = None,
+        properties: Properties | None = None,
         *args: Any,
         timeout: float | None = None,
         **kwargs: Any,
@@ -394,9 +400,9 @@ async def unsubscribe(
             **kwargs: Additional keyword arguments to pass to paho-mqtt's unsubscribe
                 method.
         """
-        result, mid = self._client.unsubscribe(topic, properties, *args, **kwargs)
+        result, mid = self._client.unsubscribe(topic, properties, *args, **kwargs)  # type: ignore[arg-type]
         # Early out on error
-        if result != mqtt.MQTT_ERR_SUCCESS:
+        if result != mqtt.MQTT_ERR_SUCCESS or mid is None:
             raise MqttCodeError(result, "Could not unsubscribe from topic")
         # Create event for when the on_unsubscribe callback is called
         confirmation = asyncio.Event()
@@ -412,7 +418,7 @@ async def publish(  # noqa: PLR0913
         payload: PayloadType = None,
         qos: int = 0,
         retain: bool = False,
-        properties: mqtt.Properties | None = None,
+        properties: Properties | None = None,
         *args: Any,
         timeout: float | None = None,
         **kwargs: Any,
@@ -518,8 +524,8 @@ def _on_connect(  # noqa: PLR0913
         client: mqtt.Client,
         userdata: Any,
         flags: dict[str, int],
-        rc: int | mqtt.ReasonCodes,
-        properties: mqtt.Properties | None = None,
+        rc: int | ReasonCode,
+        properties: Properties | None = None,
     ) -> None:
         """Called when we receive a CONNACK message from the broker."""
         # Return early if already connected. Sometimes, paho-mqtt calls _on_connect
@@ -538,8 +544,8 @@ def _on_disconnect(
         self,
         client: mqtt.Client,
         userdata: Any,
-        rc: int | mqtt.ReasonCodes | None,
-        properties: mqtt.Properties | None = None,
+        rc: int | ReasonCode | None,
+        properties: Properties | None = None,
     ) -> None:
         # Return early if the disconnect is already acknowledged.
         # Sometimes (e.g., due to timeouts), paho-mqtt calls _on_disconnect
@@ -570,8 +576,8 @@ def _on_subscribe(  # noqa: PLR0913
         client: mqtt.Client,
         userdata: Any,
         mid: int,
-        granted_qos: tuple[int] | list[mqtt.ReasonCodes],
-        properties: mqtt.Properties | None = None,
+        granted_qos: tuple[int, ...] | list[ReasonCode],
+        properties: Properties | None = None,
     ) -> None:
         """Called when we receive a SUBACK message from the broker."""
         try:
@@ -588,8 +594,8 @@ def _on_unsubscribe(  # noqa: PLR0913
         client: mqtt.Client,
         userdata: Any,
         mid: int,
-        properties: mqtt.Properties | None = None,
-        reason_codes: list[mqtt.ReasonCodes] | mqtt.ReasonCodes | None = None,
+        properties: Properties | None = None,
+        reason_codes: list[ReasonCode] | ReasonCode | None = None,
     ) -> None:
         """Called when we receive an UNSUBACK message from the broker."""
         try:
diff --git a/aiomqtt/exceptions.py b/aiomqtt/exceptions.py
index 6e5baea..3763e05 100644
--- a/aiomqtt/exceptions.py
+++ b/aiomqtt/exceptions.py
@@ -4,6 +4,7 @@
 from typing import Any
 
 import paho.mqtt.client as mqtt
+from paho.mqtt.reasoncodes import ReasonCode
 
 
 class MqttError(Exception):
@@ -11,21 +12,21 @@ class MqttError(Exception):
 
 
 class MqttCodeError(MqttError):
-    def __init__(self, rc: int | mqtt.ReasonCodes | None, *args: Any) -> None:
+    def __init__(self, rc: int | ReasonCode | None, *args: Any) -> None:
         super().__init__(*args)
         self.rc = rc
 
     def __str__(self) -> str:
-        if isinstance(self.rc, mqtt.ReasonCodes):
+        if isinstance(self.rc, ReasonCode):
             return f"[code:{self.rc.value}] {self.rc!s}"
         if isinstance(self.rc, int):
-            return f"[code:{self.rc}] {mqtt.error_string(self.rc)}"
+            return f"[code:{self.rc}] {mqtt.error_string(self.rc)}"  # type: ignore[arg-type]
         return f"[code:{self.rc}] {super().__str__()}"
 
 
 class MqttConnectError(MqttCodeError):
-    def __init__(self, rc: int | mqtt.ReasonCodes) -> None:
-        if isinstance(rc, mqtt.ReasonCodes):
+    def __init__(self, rc: int | ReasonCode) -> None:
+        if isinstance(rc, ReasonCode):
             super().__init__(rc)
             return
         msg = "Connection refused"
diff --git a/aiomqtt/message.py b/aiomqtt/message.py
index 133b67e..6f6fb27 100644
--- a/aiomqtt/message.py
+++ b/aiomqtt/message.py
@@ -4,6 +4,7 @@
 import sys
 
 import paho.mqtt.client as mqtt
+from paho.mqtt.properties import Properties
 
 if sys.version_info >= (3, 11):
     from typing import Self
@@ -50,7 +51,7 @@ def __init__(  # noqa: PLR0913
         qos: int,
         retain: bool,
         mid: int,
-        properties: mqtt.Properties | None,
+        properties: Properties | None,
     ) -> None:
         self.topic = Topic(topic) if not isinstance(topic, Topic) else topic
         self.payload = payload
diff --git a/aiomqtt/types.py b/aiomqtt/types.py
index 039a76b..61438b0 100644
--- a/aiomqtt/types.py
+++ b/aiomqtt/types.py
@@ -6,7 +6,7 @@
 import sys
 from typing import Any, Callable, TypeVar
 
-import paho.mqtt.client as mqtt
+from paho.mqtt.subscribeoptions import SubscribeOptions
 
 if sys.version_info >= (3, 10):
     from typing import ParamSpec, TypeAlias
@@ -18,10 +18,10 @@
 P = ParamSpec("P")
 
 PayloadType: TypeAlias = "str | bytes | bytearray | int | float | None"
-SubscribeTopic: TypeAlias = "str | tuple[str, mqtt.SubscribeOptions] | list[tuple[str, mqtt.SubscribeOptions]] | list[tuple[str, int]]"
+SubscribeTopic: TypeAlias = "str | tuple[str, SubscribeOptions] | list[tuple[str, SubscribeOptions]] | list[tuple[str, int]]"
 WebSocketHeaders: TypeAlias = (
     "dict[str, str] | Callable[[dict[str, str]], dict[str, str]]"
 )
-_PahoSocket: TypeAlias = "socket.socket | ssl.SSLSocket | mqtt.WebsocketWrapper | Any"
+_PahoSocket: TypeAlias = "socket.socket | ssl.SSLSocket | Any"
 # See the overloads of `socket.setsockopt` for details.
 SocketOption: TypeAlias = "tuple[int, int, int | bytes] | tuple[int, int, None, int]"
diff --git a/poetry.lock b/poetry.lock
index ce6a4a1..fd0a97d 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
 
 [[package]]
 name = "alabaster"
@@ -414,16 +414,6 @@ files = [
     {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
     {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
     {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
     {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
     {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
     {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
@@ -488,38 +478,38 @@ files = [
 
 [[package]]
 name = "mypy"
-version = "1.8.0"
+version = "1.9.0"
 description = "Optional static typing for Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"},
-    {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"},
-    {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"},
-    {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"},
-    {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"},
-    {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"},
-    {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"},
-    {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"},
-    {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"},
-    {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"},
-    {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"},
-    {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"},
-    {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"},
-    {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"},
-    {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"},
-    {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"},
-    {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"},
-    {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"},
-    {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"},
-    {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"},
-    {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"},
-    {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"},
-    {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"},
-    {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"},
-    {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"},
-    {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"},
-    {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"},
+    {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"},
+    {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"},
+    {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"},
+    {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"},
+    {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"},
+    {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"},
+    {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"},
+    {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"},
+    {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"},
+    {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"},
+    {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"},
+    {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"},
+    {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"},
+    {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"},
+    {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"},
+    {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"},
+    {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"},
+    {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"},
+    {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"},
+    {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"},
+    {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"},
+    {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"},
+    {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"},
+    {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"},
+    {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"},
+    {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"},
+    {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"},
 ]
 
 [package.dependencies]
@@ -583,16 +573,17 @@ files = [
 
 [[package]]
 name = "paho-mqtt"
-version = "1.6.1"
+version = "2.0.0"
 description = "MQTT version 5.0/3.1.1 client class"
 optional = false
-python-versions = "*"
+python-versions = ">=3.7"
 files = [
-    {file = "paho-mqtt-1.6.1.tar.gz", hash = "sha256:2a8291c81623aec00372b5a85558a372c747cbca8e9934dfe218638b8eefc26f"},
+    {file = "paho_mqtt-2.0.0-py3-none-any.whl", hash = "sha256:2ef745073dfc9aa68bfec30d0b9b6f0304ea75182bae85a7c77a80cefce1eff5"},
+    {file = "paho_mqtt-2.0.0.tar.gz", hash = "sha256:13b205f29251e4f2c66a6c923c31fc4fd780561e03b2d775cff8e4f2915cf947"},
 ]
 
 [package.extras]
-proxy = ["PySocks"]
+proxy = ["pysocks"]
 
 [[package]]
 name = "pluggy"
@@ -686,7 +677,6 @@ files = [
     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
-    {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
     {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
     {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
     {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
@@ -694,15 +684,8 @@ files = [
     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
-    {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
     {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
     {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
-    {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
-    {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
-    {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
-    {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
-    {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
-    {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
     {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
     {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
     {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
@@ -719,7 +702,6 @@ files = [
     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
-    {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
     {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
     {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
     {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
@@ -727,7 +709,6 @@ files = [
     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
-    {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
     {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
     {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
     {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
@@ -1033,17 +1014,6 @@ files = [
     {file = "tornado-6.3.3.tar.gz", hash = "sha256:e7d8db41c0181c80d76c982aacc442c0783a2c54d6400fe028954201a2e032fe"},
 ]
 
-[[package]]
-name = "types-paho-mqtt"
-version = "1.6.0.7"
-description = "Typing stubs for paho-mqtt"
-optional = false
-python-versions = "*"
-files = [
-    {file = "types-paho-mqtt-1.6.0.7.tar.gz", hash = "sha256:fe34c68abc849cd96e1482138bbdf5f465de59629dd367cb3a2423dd9ca3220b"},
-    {file = "types_paho_mqtt-1.6.0.7-py3-none-any.whl", hash = "sha256:50313d93f63d777da391acaac0278d346cf9e4a2576d814989d6500bd0ca4a35"},
-]
-
 [[package]]
 name = "typing-extensions"
 version = "4.7.1"
@@ -1090,4 +1060,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8"
-content-hash = "14edfc0e4acfb8cfc32adf72ff8e2dce8a3a41ef3c2cb9a5b74e683386868903"
+content-hash = "c3b552ed6bd7864b8437dadcec211382d038f518e811385b8dc1364780e033a0"
diff --git a/pyproject.toml b/pyproject.toml
index 069c373..4a0d018 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -20,16 +20,15 @@ classifiers = [
 
 [tool.poetry.dependencies]
 python = "^3.8"
-paho-mqtt = "^1.6.0"
+paho-mqtt = "^2.0.0"
 typing-extensions = {version = "^4.4.0", markers = "python_version < '3.10'"}
 
 [tool.poetry.group.dev]
 optional = true
 
 [tool.poetry.group.dev.dependencies]
-mypy = "^1.8.0"
+mypy = "^1.9.0"
 ruff = "^0.1.9"
-types-paho-mqtt = "^1.6.0.7"
 pytest = "^7.3.1"
 pytest-cov = "^4.0.0"
 anyio = "^3.6.2"
@@ -137,6 +136,8 @@ pretty = true
 filterwarnings = [
     "error",
     "ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning",
+    # TODO(jonathan): Remove these once we finish the migration to the new API
+    "ignore:Callback API version 1 is deprecated, update to latest version:DeprecationWarning",
 ]
 markers = [
     "network: tests that requires network access"


Test patch:
diff --git a/tests/test_client.py b/tests/test_client.py
index 333a882..6caede1 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -11,6 +11,9 @@
 import pytest
 from anyio import TASK_STATUS_IGNORED
 from anyio.abc import TaskStatus
+from paho.mqtt.enums import MQTTErrorCode
+from paho.mqtt.properties import Properties
+from paho.mqtt.subscribeoptions import SubscribeOptions
 
 from aiomqtt import (
     Client,
@@ -85,7 +88,7 @@ async def launch_client() -> None:
         tg.start_soon(launch_client)
         await event.wait()
         async with Client(HOSTNAME, will=Will(topic)) as client:
-            client._client._sock_close()  # type: ignore[attr-defined]
+            client._client._sock_close()
 
 
 @pytest.mark.network
@@ -152,7 +155,7 @@ async def handle(tg: anyio.abc.TaskGroup) -> None:
 async def test_client_logger() -> None:
     logger = logging.getLogger("aiomqtt")
     async with Client(HOSTNAME, logger=logger) as client:
-        assert logger is client._client._logger  # type: ignore[attr-defined]
+        assert logger is client._client._logger
 
 
 @pytest.mark.network
@@ -165,23 +168,24 @@ class MockPahoClient(mqtt.Client):
         def subscribe(
             self,
             topic: str
-            | tuple[str, mqtt.SubscribeOptions]
-            | list[tuple[str, mqtt.SubscribeOptions]]
-            | list[tuple[str, int]],
+            | tuple[str, int]
+            | tuple[str, SubscribeOptions]
+            | list[tuple[str, int]]
+            | list[tuple[str, SubscribeOptions]],
             qos: int = 0,
-            options: mqtt.SubscribeOptions | None = None,
-            properties: mqtt.Properties | None = None,
-        ) -> tuple[int, int]:
+            options: SubscribeOptions | None = None,
+            properties: Properties | None = None,
+        ) -> tuple[MQTTErrorCode, int | None]:
             assert client._outgoing_calls_sem is not None
             assert client._outgoing_calls_sem.locked()
             return super().subscribe(topic, qos, options, properties)
 
         def unsubscribe(
-            self, topic: str | list[str], properties: mqtt.Properties | None = None
-        ) -> tuple[int, int]:
+            self, topic: str | list[str], properties: Properties | None = None
+        ) -> tuple[MQTTErrorCode, int | None]:
             assert client._outgoing_calls_sem is not None
             assert client._outgoing_calls_sem.locked()
-            return super().unsubscribe(topic, properties)
+            return super().unsubscribe(topic, properties)  # type: ignore[arg-type]
 
         def publish(  # noqa: PLR0913
             self,
@@ -189,7 +193,7 @@ def publish(  # noqa: PLR0913
             payload: PayloadType | None = None,
             qos: int = 0,
             retain: bool = False,
-            properties: mqtt.Properties | None = None,
+            properties: Properties | None = None,
         ) -> mqtt.MQTTMessageInfo:
             assert client._outgoing_calls_sem is not None
             assert client._outgoing_calls_sem.locked()
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
index 4a24928..11e445e 100644
--- a/tests/test_exceptions.py
+++ b/tests/test_exceptions.py
@@ -1,6 +1,7 @@
 import paho.mqtt.client as mqtt
 import pytest
 from paho.mqtt.packettypes import PacketTypes
+from paho.mqtt.reasoncodes import ReasonCode
 
 from aiomqtt.exceptions import _CONNECT_RC_STRINGS, MqttCodeError, MqttConnectError
 
@@ -29,7 +30,7 @@
     ],
 )
 def test_mqtt_code_error_int(rc: int) -> None:
-    assert str(MqttCodeError(rc)) == f"[code:{rc}] {mqtt.error_string(rc)}"
+    assert str(MqttCodeError(rc)) == f"[code:{rc}] {mqtt.error_string(rc)}"  # type: ignore[arg-type]
 
 
 @pytest.mark.parametrize(
@@ -41,7 +42,7 @@ def test_mqtt_code_error_int(rc: int) -> None:
     ],
 )
 def test_mqtt_code_error_reason_codes(packet_type: int, a_name: str) -> None:
-    rc = mqtt.ReasonCodes(packet_type, a_name)
+    rc = ReasonCode(packet_type, a_name)  # type: ignore[no-untyped-call]
     assert str(MqttCodeError(rc)) == f"[code:{rc.value}] {rc!s}"
 
 
@@ -56,7 +57,7 @@ def test_mqtt_connect_error_int(rc: int, message: str) -> None:
     if rc in _CONNECT_RC_STRINGS:
         arg += f": {message}"
     assert error.args[0] == arg
-    assert str(error) == f"[code:{rc}] {mqtt.error_string(rc)}"
+    assert str(error) == f"[code:{rc}] {mqtt.error_string(rc)}"  # type: ignore[arg-type]
 
 
 @pytest.mark.parametrize(
@@ -68,5 +69,5 @@ def test_mqtt_connect_error_int(rc: int, message: str) -> None:
     ],
 )
 def test_mqtt_connect_error_reason_codes(packet_type: int, a_name: str) -> None:
-    rc = mqtt.ReasonCodes(packet_type, a_name)
+    rc = ReasonCode(packet_type, a_name)  # type: ignore[no-untyped-call]
     assert str(MqttConnectError(rc)) == f"[code:{rc.value}] {rc!s}"