place-integration-api 0.2.1__tar.gz → 0.2.3__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.1/src/place_integration_api.egg-info → place_integration_api-0.2.3}/PKG-INFO +4 -2
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/pyproject.toml +8 -2
- {place_integration_api-0.2.1 → place_integration_api-0.2.3/src/place_integration_api.egg-info}/PKG-INFO +4 -2
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/src/place_integration_api.egg-info/SOURCES.txt +1 -0
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/src/place_integration_api.egg-info/requires.txt +3 -1
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/tests/test_auth.py +7 -21
- place_integration_api-0.2.3/tests/test_models.py +88 -0
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/tests/test_mqtt_client.py +7 -10
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/tests/test_provider.py +7 -11
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/LICENSE +0 -0
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/README.md +0 -0
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/setup.cfg +0 -0
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/src/place/auth/__init__.py +0 -0
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/src/place/auth/abstract_auth.py +0 -0
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/src/place/auth/aws_srp.py +0 -0
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/src/place/auth/srp_auth.py +0 -0
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/src/place/auth/tokens.py +0 -0
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/src/place/config.py +0 -0
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/src/place/messages.py +0 -0
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/src/place/models/__init__.py +0 -0
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/src/place/models/credentials.py +0 -0
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/src/place/models/device_shadow.py +0 -0
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/src/place/models/discover_device.py +0 -0
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/src/place/models/models.py +0 -0
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/src/place/models/mqtt_message.py +0 -0
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/src/place/mqtt_client.py +0 -0
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/src/place/provider.py +0 -0
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/src/place_integration_api.egg-info/dependency_links.txt +0 -0
- {place_integration_api-0.2.1 → place_integration_api-0.2.3}/src/place_integration_api.egg-info/top_level.txt +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.3
|
|
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://pypi.org/project/place-integration-api/
|
|
@@ -15,9 +15,11 @@ Requires-Dist: aiohttp
|
|
|
15
15
|
Requires-Dist: paho-mqtt
|
|
16
16
|
Requires-Dist: six
|
|
17
17
|
Requires-Dist: python-decouple
|
|
18
|
-
Requires-Dist:
|
|
18
|
+
Requires-Dist: PyJWT
|
|
19
19
|
Provides-Extra: dev
|
|
20
20
|
Requires-Dist: basedpyright; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
21
23
|
Dynamic: license-file
|
|
22
24
|
|
|
23
25
|
# Place integration API
|
|
@@ -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.3"
|
|
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"
|
|
@@ -23,16 +23,22 @@ dependencies = [
|
|
|
23
23
|
"paho-mqtt",
|
|
24
24
|
"six",
|
|
25
25
|
"python-decouple",
|
|
26
|
-
"
|
|
26
|
+
"PyJWT"
|
|
27
27
|
]
|
|
28
28
|
|
|
29
29
|
[project.optional-dependencies]
|
|
30
30
|
dev = [
|
|
31
31
|
"basedpyright",
|
|
32
|
+
"pytest",
|
|
33
|
+
"pytest-asyncio",
|
|
32
34
|
]
|
|
33
35
|
|
|
34
36
|
[project.urls]
|
|
35
37
|
Homepage = "https://pypi.org/project/place-integration-api/"
|
|
36
38
|
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
testpaths = ["tests"]
|
|
41
|
+
asyncio_mode = "auto"
|
|
42
|
+
|
|
37
43
|
[tool.setuptools.packages.find]
|
|
38
44
|
where = ["src"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: place-integration-api
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
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://pypi.org/project/place-integration-api/
|
|
@@ -15,9 +15,11 @@ Requires-Dist: aiohttp
|
|
|
15
15
|
Requires-Dist: paho-mqtt
|
|
16
16
|
Requires-Dist: six
|
|
17
17
|
Requires-Dist: python-decouple
|
|
18
|
-
Requires-Dist:
|
|
18
|
+
Requires-Dist: PyJWT
|
|
19
19
|
Provides-Extra: dev
|
|
20
20
|
Requires-Dist: basedpyright; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
21
23
|
Dynamic: license-file
|
|
22
24
|
|
|
23
25
|
# Place integration API
|
|
@@ -1,24 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from unittest.mock import MagicMock, patch
|
|
4
|
+
from place.auth import get_iot_credentials
|
|
4
5
|
|
|
5
|
-
from place_integration_api.auth import get_credentials_via_cognito
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@patch("place_integration_api.auth.srp_auth.get_tokens_via_srp")
|
|
10
|
-
def test_get_credentials_via_cognito_success(
|
|
11
|
-
mock_get_tokens_via_srp: MagicMock, mock_boto3: MagicMock
|
|
12
|
-
) -> None:
|
|
7
|
+
@patch("place.auth.srp_auth.boto3")
|
|
8
|
+
def test_get_iot_credentials_success(mock_boto3: MagicMock) -> None:
|
|
13
9
|
identity_client = MagicMock()
|
|
14
10
|
mock_boto3.client.return_value = identity_client
|
|
15
11
|
|
|
16
|
-
mock_get_tokens_via_srp.return_value = {
|
|
17
|
-
"AuthenticationResult": {
|
|
18
|
-
"IdToken": "id-token",
|
|
19
|
-
"AccessToken": "access-token",
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
12
|
identity_client.get_id.return_value = {"IdentityId": "identity-123"}
|
|
23
13
|
identity_client.get_credentials_for_identity.return_value = {
|
|
24
14
|
"Credentials": {
|
|
@@ -28,13 +18,10 @@ def test_get_credentials_via_cognito_success(
|
|
|
28
18
|
}
|
|
29
19
|
}
|
|
30
20
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
username="user",
|
|
36
|
-
password="pass",
|
|
37
|
-
region="us-east-2",
|
|
21
|
+
|
|
22
|
+
creds = get_iot_credentials(
|
|
23
|
+
id_token="id-token",
|
|
24
|
+
access_token="access-token",
|
|
38
25
|
)
|
|
39
26
|
|
|
40
27
|
assert creds.access_key_id == "AKIA..."
|
|
@@ -42,4 +29,3 @@ def test_get_credentials_via_cognito_success(
|
|
|
42
29
|
assert creds.session_token == "session"
|
|
43
30
|
assert creds.identity_id == "identity-123"
|
|
44
31
|
assert creds.access_token == "access-token"
|
|
45
|
-
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Tests for the Place device shadow data models."""
|
|
2
|
+
|
|
3
|
+
from place.models.device_shadow import AlarmStatus, PlaceDeviceShadow
|
|
4
|
+
|
|
5
|
+
FULL_SHADOW = {
|
|
6
|
+
"state": {
|
|
7
|
+
"reported": {
|
|
8
|
+
"deviceId": "device-001",
|
|
9
|
+
"model": "MODEL-X",
|
|
10
|
+
"fwPackageId": "fw-1.0",
|
|
11
|
+
"autoUpdate": True,
|
|
12
|
+
"secureBuild": True,
|
|
13
|
+
"coAlarmStatus": 0,
|
|
14
|
+
"heatAlarmStatus": 3,
|
|
15
|
+
"smokeAlarmStatus": 5,
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_from_shadow_full() -> None:
|
|
22
|
+
"""Test parsing a shadow payload."""
|
|
23
|
+
shadow = PlaceDeviceShadow.from_shadow(FULL_SHADOW)
|
|
24
|
+
|
|
25
|
+
# Alarm statuses
|
|
26
|
+
assert shadow.co_alarm_status is AlarmStatus.IDLE
|
|
27
|
+
assert shadow.heat_alarm_status is AlarmStatus.ALARM
|
|
28
|
+
assert shadow.smoke_alarm_status is AlarmStatus.HUSHED
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_from_shadow_without_state_wrapper() -> None:
|
|
32
|
+
"""Test parsing a flat reported dict (no state.reported wrapper)."""
|
|
33
|
+
reported = {"coAlarmStatus": 3, "heatAlarmStatus": 0, "smokeAlarmStatus": 5}
|
|
34
|
+
shadow = PlaceDeviceShadow.from_shadow(reported)
|
|
35
|
+
|
|
36
|
+
assert shadow.co_alarm_status is AlarmStatus.ALARM
|
|
37
|
+
assert shadow.heat_alarm_status is AlarmStatus.IDLE
|
|
38
|
+
assert shadow.smoke_alarm_status is AlarmStatus.HUSHED
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_from_shadow_empty() -> None:
|
|
42
|
+
"""Test parsing an empty shadow returns defaults."""
|
|
43
|
+
shadow = PlaceDeviceShadow.from_shadow({})
|
|
44
|
+
|
|
45
|
+
assert shadow.co_alarm_status is AlarmStatus.NOT_PRESENT
|
|
46
|
+
assert shadow.heat_alarm_status is AlarmStatus.NOT_PRESENT
|
|
47
|
+
assert shadow.smoke_alarm_status is AlarmStatus.NOT_PRESENT
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_from_shadow_invalid_alarm_value() -> None:
|
|
51
|
+
"""Test that out-of-range alarm values default to NOT_PRESENT."""
|
|
52
|
+
shadow = PlaceDeviceShadow.from_shadow({"coAlarmStatus": 99})
|
|
53
|
+
|
|
54
|
+
assert shadow.co_alarm_status is AlarmStatus.NOT_PRESENT
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_from_shadow_null_alarm_value() -> None:
|
|
58
|
+
"""Test that null alarm values default to NOT_PRESENT."""
|
|
59
|
+
shadow = PlaceDeviceShadow.from_shadow({"coAlarmStatus": None})
|
|
60
|
+
|
|
61
|
+
assert shadow.co_alarm_status is AlarmStatus.NOT_PRESENT
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_merge_sparse_update() -> None:
|
|
65
|
+
"""Test that a sparse update only changes provided fields."""
|
|
66
|
+
shadow = PlaceDeviceShadow.from_shadow(FULL_SHADOW)
|
|
67
|
+
|
|
68
|
+
assert shadow.co_alarm_status is AlarmStatus.IDLE
|
|
69
|
+
|
|
70
|
+
shadow.merge({"state": {"reported": {"coAlarmStatus": 3}}})
|
|
71
|
+
|
|
72
|
+
# Updated fields
|
|
73
|
+
assert shadow.co_alarm_status is AlarmStatus.ALARM
|
|
74
|
+
|
|
75
|
+
# Unchanged fields
|
|
76
|
+
assert shadow.heat_alarm_status is AlarmStatus.ALARM
|
|
77
|
+
assert shadow.smoke_alarm_status is AlarmStatus.HUSHED
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_alarm_status_enum_values() -> None:
|
|
81
|
+
"""Test AlarmStatus enum has correct integer mappings."""
|
|
82
|
+
assert AlarmStatus.IDLE == 0
|
|
83
|
+
assert AlarmStatus.TEST == 1
|
|
84
|
+
assert AlarmStatus.PRE_ALARM == 2
|
|
85
|
+
assert AlarmStatus.ALARM == 3
|
|
86
|
+
assert AlarmStatus.CRITICAL_ALARM == 4
|
|
87
|
+
assert AlarmStatus.HUSHED == 5
|
|
88
|
+
assert AlarmStatus.NOT_PRESENT == 6
|
|
@@ -2,8 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from unittest.mock import MagicMock, patch
|
|
4
4
|
|
|
5
|
-
from
|
|
6
|
-
from
|
|
5
|
+
from place.models import Credentials
|
|
6
|
+
from place.mqtt_client import get_signed_uri, MqttClient
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def test_get_signed_uri_includes_host_and_token() -> None:
|
|
@@ -19,9 +19,9 @@ def test_get_signed_uri_includes_host_and_token() -> None:
|
|
|
19
19
|
assert "X-Amz-Security-Token=token123" in uri
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
@patch("
|
|
23
|
-
@patch("
|
|
24
|
-
def
|
|
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
25
|
mock_get_signed_uri: MagicMock,
|
|
26
26
|
mock_client_cls: MagicMock,
|
|
27
27
|
) -> None:
|
|
@@ -36,16 +36,13 @@ def test_run_mqtt_flow_sets_up_client(
|
|
|
36
36
|
identity_id="identity-123",
|
|
37
37
|
)
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
mqtt_client = MqttClient(
|
|
40
40
|
endpoint="example.iot.amazonaws.com",
|
|
41
41
|
credentials=creds,
|
|
42
|
-
household_ids=["hh1"],
|
|
43
|
-
thing_names=["thing1"],
|
|
44
42
|
)
|
|
43
|
+
mqtt_client.connect()
|
|
45
44
|
|
|
46
45
|
mock_get_signed_uri.assert_called_once()
|
|
47
46
|
client.ws_set_options.assert_called()
|
|
48
47
|
client.tls_set.assert_called_once()
|
|
49
48
|
client.connect.assert_called_once()
|
|
50
|
-
client.loop_forever.assert_called_once()
|
|
51
|
-
|
|
@@ -2,9 +2,9 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
|
|
5
|
-
from
|
|
6
|
-
from
|
|
7
|
-
from
|
|
5
|
+
from place.auth.abstract_auth import AbstractAuth
|
|
6
|
+
from place.config import FULFILLMENT_URL
|
|
7
|
+
from place.provider import Provider
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def test_provider_discover_parses_response() -> None:
|
|
@@ -12,8 +12,8 @@ def test_provider_discover_parses_response() -> None:
|
|
|
12
12
|
"success": True,
|
|
13
13
|
"data": {
|
|
14
14
|
"devices": [
|
|
15
|
-
{"
|
|
16
|
-
{"
|
|
15
|
+
{"thingName": "t1", "deviceId": "d1", "deviceName": "Device 1"},
|
|
16
|
+
{"thingName": "t2", "deviceId": "d2", "deviceName": "Device 2"},
|
|
17
17
|
]
|
|
18
18
|
},
|
|
19
19
|
}
|
|
@@ -40,16 +40,13 @@ def test_provider_discover_parses_response() -> None:
|
|
|
40
40
|
|
|
41
41
|
assert auth.calls == [("POST", FULFILLMENT_URL, {"json": {"command": "DISCOVER", "data": {}}})]
|
|
42
42
|
|
|
43
|
-
household_ids = sorted(
|
|
44
|
-
{d.household_id for d in devices if d.household_id is not None}
|
|
45
|
-
)
|
|
46
43
|
thing_names = sorted(
|
|
47
44
|
{d.thing_name for d in devices if d.thing_name is not None}
|
|
48
45
|
)
|
|
49
46
|
|
|
50
|
-
assert household_ids == ["h1", "h2"]
|
|
51
47
|
assert thing_names == ["t1", "t2"]
|
|
52
48
|
assert [d.device_id for d in devices] == ["d1", "d2"]
|
|
49
|
+
assert [d.device_name for d in devices] == ["Device 1", "Device 2"]
|
|
53
50
|
|
|
54
51
|
|
|
55
52
|
def test_provider_enable_sends_enable_command() -> None:
|
|
@@ -90,7 +87,7 @@ def test_provider_disable_sends_disable_command() -> None:
|
|
|
90
87
|
return "token"
|
|
91
88
|
|
|
92
89
|
async def request(self, method, url, **kwargs):
|
|
93
|
-
self.calls.append((method, url,
|
|
90
|
+
self.calls.append((method, url, kwargs))
|
|
94
91
|
|
|
95
92
|
class DummyResponse:
|
|
96
93
|
async def json(self_inner):
|
|
@@ -104,4 +101,3 @@ def test_provider_disable_sends_disable_command() -> None:
|
|
|
104
101
|
|
|
105
102
|
assert auth.calls == [("POST", FULFILLMENT_URL, {"json": {"command": "DISABLE", "data": {}}})]
|
|
106
103
|
assert result == payload
|
|
107
|
-
|
|
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.1 → place_integration_api-0.2.3}/src/place/models/device_shadow.py
RENAMED
|
File without changes
|
{place_integration_api-0.2.1 → place_integration_api-0.2.3}/src/place/models/discover_device.py
RENAMED
|
File without changes
|
|
File without changes
|
{place_integration_api-0.2.1 → place_integration_api-0.2.3}/src/place/models/mqtt_message.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|