place-integration-api 0.2.6__tar.gz → 0.2.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {place_integration_api-0.2.6/src/place_integration_api.egg-info → place_integration_api-0.2.7}/PKG-INFO +1 -1
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/pyproject.toml +1 -1
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/errors.py +4 -0
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/mqtt_client.py +29 -0
- {place_integration_api-0.2.6 → place_integration_api-0.2.7/src/place_integration_api.egg-info}/PKG-INFO +1 -1
- place_integration_api-0.2.7/tests/test_mqtt_client.py +138 -0
- place_integration_api-0.2.6/tests/test_mqtt_client.py +0 -48
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/LICENSE +0 -0
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/README.md +0 -0
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/setup.cfg +0 -0
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/auth/__init__.py +0 -0
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/auth/abstract_auth.py +0 -0
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/auth/aws_srp.py +0 -0
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/auth/srp_auth.py +0 -0
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/auth/tokens.py +0 -0
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/config.py +0 -0
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/messages.py +0 -0
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/models/__init__.py +0 -0
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/models/credentials.py +0 -0
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/models/device_shadow.py +0 -0
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/models/discover_device.py +0 -0
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/models/models.py +0 -0
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/models/mqtt_message.py +0 -0
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/provider.py +0 -0
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place_integration_api.egg-info/SOURCES.txt +0 -0
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place_integration_api.egg-info/dependency_links.txt +0 -0
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place_integration_api.egg-info/requires.txt +0 -0
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place_integration_api.egg-info/top_level.txt +0 -0
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/tests/test_auth.py +0 -0
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/tests/test_models.py +0 -0
- {place_integration_api-0.2.6 → place_integration_api-0.2.7}/tests/test_provider.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: place-integration-api
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: Python APIs for Cognito-authenticated MQTT flows and Home Assistant discovery for Gentex Place.
|
|
5
5
|
Author-email: Nicholas Aelick <niaexa@syntronic.com>, Andrew Tran <andrew.tran@gentex.com>
|
|
6
6
|
Project-URL: Homepage, https://placehomesolutions.com
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "place-integration-api"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.7"
|
|
8
8
|
description = "Python APIs for Cognito-authenticated MQTT flows and Home Assistant discovery for Gentex Place."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -7,3 +7,7 @@ class PlaceApiError(Exception):
|
|
|
7
7
|
|
|
8
8
|
class PlaceFulfillmentError(PlaceApiError):
|
|
9
9
|
"""Raised when the fulfillment API reports a business-logic failure."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PlaceMqttConnectionError(PlaceApiError):
|
|
13
|
+
"""Raised when the MQTT broker rejects or fails to establish a connection."""
|
|
@@ -2,14 +2,17 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import hashlib
|
|
4
4
|
import hmac
|
|
5
|
+
import threading
|
|
5
6
|
import uuid
|
|
6
7
|
from datetime import datetime, timezone
|
|
7
8
|
from typing import Callable
|
|
8
9
|
from urllib.parse import quote
|
|
9
10
|
|
|
10
11
|
import paho.mqtt.client as mqtt
|
|
12
|
+
from paho.mqtt.reasoncodes import ReasonCode
|
|
11
13
|
|
|
12
14
|
from .config import ALGORITHM, EXPIRE_SEC, KEEP_ALIVE_SEC, PATH, REGION, SCHEME, SERVICE
|
|
15
|
+
from .errors import PlaceMqttConnectionError
|
|
13
16
|
from .models import Credentials
|
|
14
17
|
|
|
15
18
|
|
|
@@ -92,12 +95,17 @@ class MqttClient:
|
|
|
92
95
|
self.endpoint = endpoint
|
|
93
96
|
self.credentials = credentials
|
|
94
97
|
self._client: mqtt.Client | None = None
|
|
98
|
+
self._connected_event = threading.Event()
|
|
99
|
+
self._connect_error: ReasonCode | None = None
|
|
95
100
|
|
|
96
101
|
def connect(
|
|
97
102
|
self,
|
|
98
103
|
on_message: Callable[[str, bytes], None] | None = None,
|
|
99
104
|
on_connect: Callable[[], None] | None = None,
|
|
100
105
|
) -> None:
|
|
106
|
+
self._connected_event.clear()
|
|
107
|
+
self._connect_error = None
|
|
108
|
+
|
|
101
109
|
signed_uri = get_signed_uri(
|
|
102
110
|
access_key_id=self.credentials.access_key_id,
|
|
103
111
|
secret_access_key=self.credentials.secret_access_key,
|
|
@@ -119,8 +127,12 @@ class MqttClient:
|
|
|
119
127
|
def _on_connect(_client, _userdata, _flags, reason_code, _properties):
|
|
120
128
|
if reason_code.is_failure:
|
|
121
129
|
print(f"Connect failed: {reason_code}")
|
|
130
|
+
self._connect_error = reason_code
|
|
131
|
+
self._connected_event.set()
|
|
122
132
|
return
|
|
123
133
|
print("Connected")
|
|
134
|
+
self._connect_error = None
|
|
135
|
+
self._connected_event.set()
|
|
124
136
|
if on_connect:
|
|
125
137
|
try:
|
|
126
138
|
on_connect()
|
|
@@ -149,6 +161,23 @@ class MqttClient:
|
|
|
149
161
|
if self._client:
|
|
150
162
|
self._client.loop_stop()
|
|
151
163
|
|
|
164
|
+
def wait_for_connection(self, timeout: float = 10.0) -> None:
|
|
165
|
+
"""Block until the initial CONNACK is received.
|
|
166
|
+
|
|
167
|
+
Raises PlaceMqttConnectionError if the broker rejects the connection
|
|
168
|
+
or none arrives within timeout, tearing down the client in that case.
|
|
169
|
+
"""
|
|
170
|
+
if not self._connected_event.wait(timeout):
|
|
171
|
+
self.disconnect()
|
|
172
|
+
self.loop_stop()
|
|
173
|
+
raise PlaceMqttConnectionError("Timed out waiting for MQTT CONNACK")
|
|
174
|
+
if self._connect_error is not None:
|
|
175
|
+
self.disconnect()
|
|
176
|
+
self.loop_stop()
|
|
177
|
+
raise PlaceMqttConnectionError(
|
|
178
|
+
f"MQTT connection rejected: {self._connect_error}"
|
|
179
|
+
)
|
|
180
|
+
|
|
152
181
|
def subscribe(self, topic: str, qos: int = 1) -> None:
|
|
153
182
|
assert self._client is not None
|
|
154
183
|
self._client.subscribe(topic, qos=qos)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: place-integration-api
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: Python APIs for Cognito-authenticated MQTT flows and Home Assistant discovery for Gentex Place.
|
|
5
5
|
Author-email: Nicholas Aelick <niaexa@syntronic.com>, Andrew Tran <andrew.tran@gentex.com>
|
|
6
6
|
Project-URL: Homepage, https://placehomesolutions.com
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
from paho.mqtt.packettypes import PacketTypes
|
|
6
|
+
from paho.mqtt.reasoncodes import ReasonCode
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from place.errors import PlaceMqttConnectionError
|
|
10
|
+
from place.models import Credentials
|
|
11
|
+
from place.mqtt_client import get_signed_uri, MqttClient
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_get_signed_uri_includes_host_and_token() -> None:
|
|
15
|
+
uri = get_signed_uri(
|
|
16
|
+
access_key_id="AKIA...",
|
|
17
|
+
secret_access_key="secret",
|
|
18
|
+
session_token="token123",
|
|
19
|
+
host="example.iot.amazonaws.com",
|
|
20
|
+
)
|
|
21
|
+
assert uri.startswith("wss://example.iot.amazonaws.com/mqtt?")
|
|
22
|
+
assert "X-Amz-Algorithm=" in uri
|
|
23
|
+
assert "X-Amz-Signature=" in uri
|
|
24
|
+
assert "X-Amz-Security-Token=token123" in uri
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@patch("place.mqtt_client.mqtt.Client")
|
|
28
|
+
@patch("place.mqtt_client.get_signed_uri")
|
|
29
|
+
def test_mqtt_client_connect_sets_up_client(
|
|
30
|
+
mock_get_signed_uri: MagicMock,
|
|
31
|
+
mock_client_cls: MagicMock,
|
|
32
|
+
) -> None:
|
|
33
|
+
mock_get_signed_uri.return_value = "wss://example.iot.amazonaws.com/mqtt?x=1"
|
|
34
|
+
client = MagicMock()
|
|
35
|
+
mock_client_cls.return_value = client
|
|
36
|
+
|
|
37
|
+
creds = Credentials(
|
|
38
|
+
access_key_id="AKIA...",
|
|
39
|
+
secret_access_key="secret",
|
|
40
|
+
session_token="token",
|
|
41
|
+
identity_id="identity-123",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
mqtt_client = MqttClient(
|
|
45
|
+
endpoint="example.iot.amazonaws.com",
|
|
46
|
+
credentials=creds,
|
|
47
|
+
)
|
|
48
|
+
mqtt_client.connect()
|
|
49
|
+
|
|
50
|
+
mock_get_signed_uri.assert_called_once()
|
|
51
|
+
client.ws_set_options.assert_called()
|
|
52
|
+
client.tls_set.assert_called_once()
|
|
53
|
+
client.connect.assert_called_once()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@patch("place.mqtt_client.mqtt.Client")
|
|
57
|
+
@patch("place.mqtt_client.get_signed_uri")
|
|
58
|
+
def test_wait_for_connection_succeeds_on_connack_success(
|
|
59
|
+
mock_get_signed_uri: MagicMock,
|
|
60
|
+
mock_client_cls: MagicMock,
|
|
61
|
+
) -> None:
|
|
62
|
+
mock_get_signed_uri.return_value = "wss://example.iot.amazonaws.com/mqtt?x=1"
|
|
63
|
+
client = MagicMock()
|
|
64
|
+
mock_client_cls.return_value = client
|
|
65
|
+
|
|
66
|
+
creds = Credentials(
|
|
67
|
+
access_key_id="AKIA...",
|
|
68
|
+
secret_access_key="secret",
|
|
69
|
+
session_token="token",
|
|
70
|
+
identity_id="identity-123",
|
|
71
|
+
)
|
|
72
|
+
mqtt_client = MqttClient(endpoint="example.iot.amazonaws.com", credentials=creds)
|
|
73
|
+
mqtt_client.connect()
|
|
74
|
+
|
|
75
|
+
client.on_connect(
|
|
76
|
+
client, None, None, ReasonCode(PacketTypes.CONNACK, identifier=0), None
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
mqtt_client.wait_for_connection(timeout=1)
|
|
80
|
+
|
|
81
|
+
client.disconnect.assert_not_called()
|
|
82
|
+
client.loop_stop.assert_not_called()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@patch("place.mqtt_client.mqtt.Client")
|
|
86
|
+
@patch("place.mqtt_client.get_signed_uri")
|
|
87
|
+
def test_wait_for_connection_raises_on_connack_rejection(
|
|
88
|
+
mock_get_signed_uri: MagicMock,
|
|
89
|
+
mock_client_cls: MagicMock,
|
|
90
|
+
) -> None:
|
|
91
|
+
mock_get_signed_uri.return_value = "wss://example.iot.amazonaws.com/mqtt?x=1"
|
|
92
|
+
client = MagicMock()
|
|
93
|
+
mock_client_cls.return_value = client
|
|
94
|
+
|
|
95
|
+
creds = Credentials(
|
|
96
|
+
access_key_id="AKIA...",
|
|
97
|
+
secret_access_key="secret",
|
|
98
|
+
session_token="token",
|
|
99
|
+
identity_id="identity-123",
|
|
100
|
+
)
|
|
101
|
+
mqtt_client = MqttClient(endpoint="example.iot.amazonaws.com", credentials=creds)
|
|
102
|
+
mqtt_client.connect()
|
|
103
|
+
|
|
104
|
+
client.on_connect(
|
|
105
|
+
client, None, None, ReasonCode(PacketTypes.CONNACK, identifier=135), None
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
with pytest.raises(PlaceMqttConnectionError, match="rejected"):
|
|
109
|
+
mqtt_client.wait_for_connection(timeout=1)
|
|
110
|
+
|
|
111
|
+
client.disconnect.assert_called_once()
|
|
112
|
+
client.loop_stop.assert_called_once()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@patch("place.mqtt_client.mqtt.Client")
|
|
116
|
+
@patch("place.mqtt_client.get_signed_uri")
|
|
117
|
+
def test_wait_for_connection_raises_on_timeout(
|
|
118
|
+
mock_get_signed_uri: MagicMock,
|
|
119
|
+
mock_client_cls: MagicMock,
|
|
120
|
+
) -> None:
|
|
121
|
+
mock_get_signed_uri.return_value = "wss://example.iot.amazonaws.com/mqtt?x=1"
|
|
122
|
+
client = MagicMock()
|
|
123
|
+
mock_client_cls.return_value = client
|
|
124
|
+
|
|
125
|
+
creds = Credentials(
|
|
126
|
+
access_key_id="AKIA...",
|
|
127
|
+
secret_access_key="secret",
|
|
128
|
+
session_token="token",
|
|
129
|
+
identity_id="identity-123",
|
|
130
|
+
)
|
|
131
|
+
mqtt_client = MqttClient(endpoint="example.iot.amazonaws.com", credentials=creds)
|
|
132
|
+
mqtt_client.connect()
|
|
133
|
+
|
|
134
|
+
with pytest.raises(PlaceMqttConnectionError, match="Timed out"):
|
|
135
|
+
mqtt_client.wait_for_connection(timeout=0.05)
|
|
136
|
+
|
|
137
|
+
client.disconnect.assert_called_once()
|
|
138
|
+
client.loop_stop.assert_called_once()
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from unittest.mock import MagicMock, patch
|
|
4
|
-
|
|
5
|
-
from place.models import Credentials
|
|
6
|
-
from place.mqtt_client import get_signed_uri, MqttClient
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def test_get_signed_uri_includes_host_and_token() -> None:
|
|
10
|
-
uri = get_signed_uri(
|
|
11
|
-
access_key_id="AKIA...",
|
|
12
|
-
secret_access_key="secret",
|
|
13
|
-
session_token="token123",
|
|
14
|
-
host="example.iot.amazonaws.com",
|
|
15
|
-
)
|
|
16
|
-
assert uri.startswith("wss://example.iot.amazonaws.com/mqtt?")
|
|
17
|
-
assert "X-Amz-Algorithm=" in uri
|
|
18
|
-
assert "X-Amz-Signature=" in uri
|
|
19
|
-
assert "X-Amz-Security-Token=token123" in uri
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@patch("place.mqtt_client.mqtt.Client")
|
|
23
|
-
@patch("place.mqtt_client.get_signed_uri")
|
|
24
|
-
def test_mqtt_client_connect_sets_up_client(
|
|
25
|
-
mock_get_signed_uri: MagicMock,
|
|
26
|
-
mock_client_cls: MagicMock,
|
|
27
|
-
) -> None:
|
|
28
|
-
mock_get_signed_uri.return_value = "wss://example.iot.amazonaws.com/mqtt?x=1"
|
|
29
|
-
client = MagicMock()
|
|
30
|
-
mock_client_cls.return_value = client
|
|
31
|
-
|
|
32
|
-
creds = Credentials(
|
|
33
|
-
access_key_id="AKIA...",
|
|
34
|
-
secret_access_key="secret",
|
|
35
|
-
session_token="token",
|
|
36
|
-
identity_id="identity-123",
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
mqtt_client = MqttClient(
|
|
40
|
-
endpoint="example.iot.amazonaws.com",
|
|
41
|
-
credentials=creds,
|
|
42
|
-
)
|
|
43
|
-
mqtt_client.connect()
|
|
44
|
-
|
|
45
|
-
mock_get_signed_uri.assert_called_once()
|
|
46
|
-
client.ws_set_options.assert_called()
|
|
47
|
-
client.tls_set.assert_called_once()
|
|
48
|
-
client.connect.assert_called_once()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/models/device_shadow.py
RENAMED
|
File without changes
|
{place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/models/discover_device.py
RENAMED
|
File without changes
|
|
File without changes
|
{place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/models/mqtt_message.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|