place-integration-api 0.2.1__tar.gz → 0.2.4__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 (29) hide show
  1. {place_integration_api-0.2.1/src/place_integration_api.egg-info → place_integration_api-0.2.4}/PKG-INFO +4 -2
  2. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/pyproject.toml +8 -2
  3. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/src/place/messages.py +7 -0
  4. {place_integration_api-0.2.1 → place_integration_api-0.2.4/src/place_integration_api.egg-info}/PKG-INFO +4 -2
  5. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/src/place_integration_api.egg-info/SOURCES.txt +1 -0
  6. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/src/place_integration_api.egg-info/requires.txt +3 -1
  7. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/tests/test_auth.py +7 -21
  8. place_integration_api-0.2.4/tests/test_models.py +88 -0
  9. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/tests/test_mqtt_client.py +7 -10
  10. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/tests/test_provider.py +7 -11
  11. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/LICENSE +0 -0
  12. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/README.md +0 -0
  13. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/setup.cfg +0 -0
  14. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/src/place/auth/__init__.py +0 -0
  15. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/src/place/auth/abstract_auth.py +0 -0
  16. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/src/place/auth/aws_srp.py +0 -0
  17. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/src/place/auth/srp_auth.py +0 -0
  18. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/src/place/auth/tokens.py +0 -0
  19. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/src/place/config.py +0 -0
  20. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/src/place/models/__init__.py +0 -0
  21. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/src/place/models/credentials.py +0 -0
  22. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/src/place/models/device_shadow.py +0 -0
  23. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/src/place/models/discover_device.py +0 -0
  24. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/src/place/models/models.py +0 -0
  25. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/src/place/models/mqtt_message.py +0 -0
  26. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/src/place/mqtt_client.py +0 -0
  27. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/src/place/provider.py +0 -0
  28. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/src/place_integration_api.egg-info/dependency_links.txt +0 -0
  29. {place_integration_api-0.2.1 → place_integration_api-0.2.4}/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.1
3
+ Version: 0.2.4
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: jwt
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.1"
7
+ version = "0.2.4"
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
- "jwt"
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"]
@@ -84,3 +84,10 @@ class PlaceMessages:
84
84
  return describe_message(topic, raw)
85
85
 
86
86
 
87
+ @staticmethod
88
+ def thing_name_from_topic(topic: str) -> str | None:
89
+ """Extract thing_name from an AWS IoT shadow topic ($aws/things/{name}/shadow/...)."""
90
+ parts = topic.split("/")
91
+ if len(parts) >= 3 and parts[0] == "$aws" and parts[1] == "things":
92
+ return parts[2]
93
+ return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: place-integration-api
3
- Version: 0.2.1
3
+ Version: 0.2.4
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: jwt
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
@@ -22,5 +22,6 @@ src/place_integration_api.egg-info/dependency_links.txt
22
22
  src/place_integration_api.egg-info/requires.txt
23
23
  src/place_integration_api.egg-info/top_level.txt
24
24
  tests/test_auth.py
25
+ tests/test_models.py
25
26
  tests/test_mqtt_client.py
26
27
  tests/test_provider.py
@@ -3,7 +3,9 @@ aiohttp
3
3
  paho-mqtt
4
4
  six
5
5
  python-decouple
6
- jwt
6
+ PyJWT
7
7
 
8
8
  [dev]
9
9
  basedpyright
10
+ pytest
11
+ pytest-asyncio
@@ -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
- @patch("place_integration_api.auth.srp_auth.boto3")
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
- creds = get_credentials_via_cognito(
32
- user_pool_id="pool",
33
- client_id="client",
34
- identity_pool_id="identity-pool",
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 place_integration_api.models import Credentials
6
- from place_integration_api.mqtt_client import get_signed_uri, run_mqtt_flow
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("place_integration_api.mqtt_client.mqtt.Client")
23
- @patch("place_integration_api.mqtt_client.get_signed_uri")
24
- def test_run_mqtt_flow_sets_up_client(
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
- run_mqtt_flow(
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 place_integration_api.auth.abstract_auth import AbstractAuth
6
- from place_integration_api.config import FULFILLMENT_URL
7
- from place_integration_api.provider import Provider
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
- {"householdId": "h1", "thingName": "t1", "deviceId": "d1"},
16
- {"householdId": "h2", "thingName": "t2", "deviceId": "d2"},
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, **kwargs))
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
-