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.
Files changed (31) hide show
  1. {place_integration_api-0.2.6/src/place_integration_api.egg-info → place_integration_api-0.2.7}/PKG-INFO +1 -1
  2. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/pyproject.toml +1 -1
  3. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/errors.py +4 -0
  4. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/mqtt_client.py +29 -0
  5. {place_integration_api-0.2.6 → place_integration_api-0.2.7/src/place_integration_api.egg-info}/PKG-INFO +1 -1
  6. place_integration_api-0.2.7/tests/test_mqtt_client.py +138 -0
  7. place_integration_api-0.2.6/tests/test_mqtt_client.py +0 -48
  8. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/LICENSE +0 -0
  9. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/README.md +0 -0
  10. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/setup.cfg +0 -0
  11. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/auth/__init__.py +0 -0
  12. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/auth/abstract_auth.py +0 -0
  13. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/auth/aws_srp.py +0 -0
  14. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/auth/srp_auth.py +0 -0
  15. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/auth/tokens.py +0 -0
  16. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/config.py +0 -0
  17. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/messages.py +0 -0
  18. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/models/__init__.py +0 -0
  19. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/models/credentials.py +0 -0
  20. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/models/device_shadow.py +0 -0
  21. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/models/discover_device.py +0 -0
  22. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/models/models.py +0 -0
  23. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/models/mqtt_message.py +0 -0
  24. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place/provider.py +0 -0
  25. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place_integration_api.egg-info/SOURCES.txt +0 -0
  26. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place_integration_api.egg-info/dependency_links.txt +0 -0
  27. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place_integration_api.egg-info/requires.txt +0 -0
  28. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/src/place_integration_api.egg-info/top_level.txt +0 -0
  29. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/tests/test_auth.py +0 -0
  30. {place_integration_api-0.2.6 → place_integration_api-0.2.7}/tests/test_models.py +0 -0
  31. {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.6
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.6"
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.6
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()