axis 70__tar.gz → 71__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 (113) hide show
  1. {axis-70 → axis-71}/PKG-INFO +2 -2
  2. {axis-70 → axis-71}/axis/interfaces/mqtt.py +2 -0
  3. {axis-70 → axis-71}/axis/models/event.py +27 -1
  4. {axis-70 → axis-71}/axis/websocket.py +2 -0
  5. {axis-70 → axis-71}/axis.egg-info/PKG-INFO +2 -2
  6. {axis-70 → axis-71}/axis.egg-info/requires.txt +1 -1
  7. {axis-70 → axis-71}/pyproject.toml +2 -2
  8. {axis-70 → axis-71}/tests/test_event.py +74 -0
  9. {axis-70 → axis-71}/tests/test_event_stream.py +26 -0
  10. {axis-70 → axis-71}/tests/test_mqtt.py +15 -0
  11. {axis-70 → axis-71}/tests/test_websocket.py +49 -0
  12. {axis-70 → axis-71}/LICENSE +0 -0
  13. {axis-70 → axis-71}/README.md +0 -0
  14. {axis-70 → axis-71}/axis/__init__.py +0 -0
  15. {axis-70 → axis-71}/axis/__main__.py +0 -0
  16. {axis-70 → axis-71}/axis/device.py +0 -0
  17. {axis-70 → axis-71}/axis/errors.py +0 -0
  18. {axis-70 → axis-71}/axis/interfaces/__init__.py +0 -0
  19. {axis-70 → axis-71}/axis/interfaces/aiohttp_digest.py +0 -0
  20. {axis-70 → axis-71}/axis/interfaces/api_discovery.py +0 -0
  21. {axis-70 → axis-71}/axis/interfaces/api_handler.py +0 -0
  22. {axis-70 → axis-71}/axis/interfaces/applications/__init__.py +0 -0
  23. {axis-70 → axis-71}/axis/interfaces/applications/application_handler.py +0 -0
  24. {axis-70 → axis-71}/axis/interfaces/applications/applications.py +0 -0
  25. {axis-70 → axis-71}/axis/interfaces/applications/fence_guard.py +0 -0
  26. {axis-70 → axis-71}/axis/interfaces/applications/loitering_guard.py +0 -0
  27. {axis-70 → axis-71}/axis/interfaces/applications/motion_guard.py +0 -0
  28. {axis-70 → axis-71}/axis/interfaces/applications/object_analytics.py +0 -0
  29. {axis-70 → axis-71}/axis/interfaces/applications/vmd4.py +0 -0
  30. {axis-70 → axis-71}/axis/interfaces/basic_device_info.py +0 -0
  31. {axis-70 → axis-71}/axis/interfaces/event_instances.py +0 -0
  32. {axis-70 → axis-71}/axis/interfaces/event_manager.py +0 -0
  33. {axis-70 → axis-71}/axis/interfaces/light_control.py +0 -0
  34. {axis-70 → axis-71}/axis/interfaces/parameters/__init__.py +0 -0
  35. {axis-70 → axis-71}/axis/interfaces/parameters/brand.py +0 -0
  36. {axis-70 → axis-71}/axis/interfaces/parameters/image.py +0 -0
  37. {axis-70 → axis-71}/axis/interfaces/parameters/io_port.py +0 -0
  38. {axis-70 → axis-71}/axis/interfaces/parameters/param_cgi.py +0 -0
  39. {axis-70 → axis-71}/axis/interfaces/parameters/param_handler.py +0 -0
  40. {axis-70 → axis-71}/axis/interfaces/parameters/properties.py +0 -0
  41. {axis-70 → axis-71}/axis/interfaces/parameters/ptz.py +0 -0
  42. {axis-70 → axis-71}/axis/interfaces/parameters/stream_profile.py +0 -0
  43. {axis-70 → axis-71}/axis/interfaces/pir_sensor_configuration.py +0 -0
  44. {axis-70 → axis-71}/axis/interfaces/port_cgi.py +0 -0
  45. {axis-70 → axis-71}/axis/interfaces/port_management.py +0 -0
  46. {axis-70 → axis-71}/axis/interfaces/ptz.py +0 -0
  47. {axis-70 → axis-71}/axis/interfaces/pwdgrp_cgi.py +0 -0
  48. {axis-70 → axis-71}/axis/interfaces/stream_profiles.py +0 -0
  49. {axis-70 → axis-71}/axis/interfaces/user_groups.py +0 -0
  50. {axis-70 → axis-71}/axis/interfaces/vapix.py +0 -0
  51. {axis-70 → axis-71}/axis/interfaces/view_areas.py +0 -0
  52. {axis-70 → axis-71}/axis/models/__init__.py +0 -0
  53. {axis-70 → axis-71}/axis/models/api.py +0 -0
  54. {axis-70 → axis-71}/axis/models/api_discovery.py +0 -0
  55. {axis-70 → axis-71}/axis/models/applications/__init__.py +0 -0
  56. {axis-70 → axis-71}/axis/models/applications/application.py +0 -0
  57. {axis-70 → axis-71}/axis/models/applications/fence_guard.py +0 -0
  58. {axis-70 → axis-71}/axis/models/applications/loitering_guard.py +0 -0
  59. {axis-70 → axis-71}/axis/models/applications/motion_guard.py +0 -0
  60. {axis-70 → axis-71}/axis/models/applications/object_analytics.py +0 -0
  61. {axis-70 → axis-71}/axis/models/applications/vmd4.py +0 -0
  62. {axis-70 → axis-71}/axis/models/basic_device_info.py +0 -0
  63. {axis-70 → axis-71}/axis/models/configuration.py +0 -0
  64. {axis-70 → axis-71}/axis/models/event_instance.py +0 -0
  65. {axis-70 → axis-71}/axis/models/light_control.py +0 -0
  66. {axis-70 → axis-71}/axis/models/mqtt.py +0 -0
  67. {axis-70 → axis-71}/axis/models/parameters/__init__.py +0 -0
  68. {axis-70 → axis-71}/axis/models/parameters/brand.py +0 -0
  69. {axis-70 → axis-71}/axis/models/parameters/image.py +0 -0
  70. {axis-70 → axis-71}/axis/models/parameters/io_port.py +0 -0
  71. {axis-70 → axis-71}/axis/models/parameters/param_cgi.py +0 -0
  72. {axis-70 → axis-71}/axis/models/parameters/properties.py +0 -0
  73. {axis-70 → axis-71}/axis/models/parameters/ptz.py +0 -0
  74. {axis-70 → axis-71}/axis/models/parameters/stream_profile.py +0 -0
  75. {axis-70 → axis-71}/axis/models/pir_sensor_configuration.py +0 -0
  76. {axis-70 → axis-71}/axis/models/port_cgi.py +0 -0
  77. {axis-70 → axis-71}/axis/models/port_management.py +0 -0
  78. {axis-70 → axis-71}/axis/models/ptz_cgi.py +0 -0
  79. {axis-70 → axis-71}/axis/models/pwdgrp_cgi.py +0 -0
  80. {axis-70 → axis-71}/axis/models/stream_profile.py +0 -0
  81. {axis-70 → axis-71}/axis/models/user_group.py +0 -0
  82. {axis-70 → axis-71}/axis/models/view_area.py +0 -0
  83. {axis-70 → axis-71}/axis/py.typed +0 -0
  84. {axis-70 → axis-71}/axis/rtsp.py +0 -0
  85. {axis-70 → axis-71}/axis/stream_manager.py +0 -0
  86. {axis-70 → axis-71}/axis/stream_transport.py +0 -0
  87. {axis-70 → axis-71}/axis.egg-info/SOURCES.txt +0 -0
  88. {axis-70 → axis-71}/axis.egg-info/dependency_links.txt +0 -0
  89. {axis-70 → axis-71}/axis.egg-info/entry_points.txt +0 -0
  90. {axis-70 → axis-71}/axis.egg-info/top_level.txt +0 -0
  91. {axis-70 → axis-71}/setup.cfg +0 -0
  92. {axis-70 → axis-71}/tests/test_api_discovery.py +0 -0
  93. {axis-70 → axis-71}/tests/test_api_handler.py +0 -0
  94. {axis-70 → axis-71}/tests/test_auth_scheme.py +0 -0
  95. {axis-70 → axis-71}/tests/test_basic_device_info.py +0 -0
  96. {axis-70 → axis-71}/tests/test_configuration.py +0 -0
  97. {axis-70 → axis-71}/tests/test_conftest.py +0 -0
  98. {axis-70 → axis-71}/tests/test_device.py +0 -0
  99. {axis-70 → axis-71}/tests/test_event_instances.py +0 -0
  100. {axis-70 → axis-71}/tests/test_http_client_compat.py +0 -0
  101. {axis-70 → axis-71}/tests/test_light_control.py +0 -0
  102. {axis-70 → axis-71}/tests/test_main_http_client.py +0 -0
  103. {axis-70 → axis-71}/tests/test_pir_sensor_configuration.py +0 -0
  104. {axis-70 → axis-71}/tests/test_port_cgi.py +0 -0
  105. {axis-70 → axis-71}/tests/test_port_management.py +0 -0
  106. {axis-70 → axis-71}/tests/test_ptz.py +0 -0
  107. {axis-70 → axis-71}/tests/test_pwdgrp_cgi.py +0 -0
  108. {axis-70 → axis-71}/tests/test_rtsp.py +0 -0
  109. {axis-70 → axis-71}/tests/test_stream_manager.py +0 -0
  110. {axis-70 → axis-71}/tests/test_stream_profiles.py +0 -0
  111. {axis-70 → axis-71}/tests/test_user_groups.py +0 -0
  112. {axis-70 → axis-71}/tests/test_vapix.py +0 -0
  113. {axis-70 → axis-71}/tests/test_view_areas.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: axis
3
- Version: 70
3
+ Version: 71
4
4
  Summary: A Python library for communicating with devices from Axis Communications
5
5
  Author-email: Robert Svensson <Kane610@users.noreply.github.com>
6
6
  License: MIT
@@ -34,7 +34,7 @@ Requires-Dist: pytest-aiohttp==1.1.0; extra == "requirements-test"
34
34
  Requires-Dist: pytest-asyncio==1.3.0; extra == "requirements-test"
35
35
  Requires-Dist: pytest-cov==7.1.0; extra == "requirements-test"
36
36
  Requires-Dist: ruff==0.15.12; extra == "requirements-test"
37
- Requires-Dist: types-xmltodict==v1.0.1.20260408; extra == "requirements-test"
37
+ Requires-Dist: types-xmltodict==v1.0.1.20260508; extra == "requirements-test"
38
38
  Provides-Extra: requirements-dev
39
39
  Requires-Dist: pre-commit==4.6.0; extra == "requirements-dev"
40
40
  Dynamic: license-file
@@ -42,6 +42,8 @@ def mqtt_json_to_event(msg: bytes | bytearray | memoryview | str) -> dict[str, A
42
42
  source, source_idx = next(iter(source_dict.items()))
43
43
  if data_dict := msg_message.get("data"):
44
44
  data_type, data_value = next(iter(data_dict.items()))
45
+ if "active" in data_dict:
46
+ data_type, data_value = "active", data_dict["active"]
45
47
 
46
48
  return {
47
49
  "topic": topic,
@@ -91,6 +91,9 @@ TOPIC_TO_STATE = {
91
91
  EventTopic.RELAY: "active",
92
92
  }
93
93
 
94
+ KNOWN_FALSE_STATES = {"", "0", "false", "inactive", "low", "off"}
95
+ KNOWN_TRUE_STATES = {"1", "active", "high", "on", "true"}
96
+
94
97
  EVENT_OPERATION = "operation"
95
98
  EVENT_SOURCE = "source"
96
99
  EVENT_SOURCE_IDX = "source_idx"
@@ -138,6 +141,28 @@ def extract_name_value(
138
141
  return item.get("Name", ""), item.get("Value", "")
139
142
 
140
143
 
144
+ def is_tripped(value: object, topic_base: EventTopic, event_type: object) -> bool:
145
+ """Return whether an event value should be considered active/tripped."""
146
+ if (expected_state := TOPIC_TO_STATE.get(topic_base)) is not None:
147
+ return str(value) == expected_state
148
+
149
+ value_text = str(value).strip()
150
+ state = value_text.casefold()
151
+
152
+ if state in KNOWN_FALSE_STATES:
153
+ return False
154
+
155
+ if state in KNOWN_TRUE_STATES:
156
+ return True
157
+
158
+ if str(event_type).casefold() == "active":
159
+ return False
160
+
161
+ # Non-empty semantic values (for example classification values like "human")
162
+ # are treated as stateless event triggers.
163
+ return bool(value_text)
164
+
165
+
141
166
  @dataclass
142
167
  class Event:
143
168
  """Event data from Axis device."""
@@ -166,6 +191,7 @@ class Event:
166
191
  topic = data.get(EVENT_TOPIC, "")
167
192
  source = data.get(EVENT_SOURCE, "")
168
193
  source_idx = data.get(EVENT_SOURCE_IDX, "")
194
+ event_type = data.get(EVENT_TYPE, "")
169
195
  value = data.get(EVENT_VALUE, "")
170
196
 
171
197
  if (topic_base := EventTopic(topic)) is EventTopic.UNKNOWN:
@@ -181,7 +207,7 @@ class Event:
181
207
  data=data,
182
208
  group=TOPIC_TO_GROUP.get(topic_base, EventGroup.NONE),
183
209
  id=source_idx,
184
- is_tripped=value == TOPIC_TO_STATE.get(topic_base, "1"),
210
+ is_tripped=is_tripped(value, topic_base, event_type),
185
211
  operation=operation,
186
212
  source=source,
187
213
  state=value,
@@ -113,6 +113,8 @@ def _parse_ws_notification(notification: dict[str, Any]) -> dict[str, Any]:
113
113
 
114
114
  source, source_idx = next(iter(source_dict.items()), ("", ""))
115
115
  data_type, data_value = next(iter(data_dict.items()), ("", ""))
116
+ if "active" in data_dict:
117
+ data_type, data_value = "active", data_dict["active"]
116
118
 
117
119
  return {
118
120
  EVENT_TOPIC: topic,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: axis
3
- Version: 70
3
+ Version: 71
4
4
  Summary: A Python library for communicating with devices from Axis Communications
5
5
  Author-email: Robert Svensson <Kane610@users.noreply.github.com>
6
6
  License: MIT
@@ -34,7 +34,7 @@ Requires-Dist: pytest-aiohttp==1.1.0; extra == "requirements-test"
34
34
  Requires-Dist: pytest-asyncio==1.3.0; extra == "requirements-test"
35
35
  Requires-Dist: pytest-cov==7.1.0; extra == "requirements-test"
36
36
  Requires-Dist: ruff==0.15.12; extra == "requirements-test"
37
- Requires-Dist: types-xmltodict==v1.0.1.20260408; extra == "requirements-test"
37
+ Requires-Dist: types-xmltodict==v1.0.1.20260508; extra == "requirements-test"
38
38
  Provides-Extra: requirements-dev
39
39
  Requires-Dist: pre-commit==4.6.0; extra == "requirements-dev"
40
40
  Dynamic: license-file
@@ -20,4 +20,4 @@ pytest-aiohttp==1.1.0
20
20
  pytest-asyncio==1.3.0
21
21
  pytest-cov==7.1.0
22
22
  ruff==0.15.12
23
- types-xmltodict==v1.0.1.20260408
23
+ types-xmltodict==v1.0.1.20260508
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "axis"
7
- version = "70"
7
+ version = "71"
8
8
  license = {text = "MIT"}
9
9
  description = "A Python library for communicating with devices from Axis Communications"
10
10
  readme = "README.md"
@@ -41,7 +41,7 @@ requirements-test = [
41
41
  "pytest-asyncio==1.3.0",
42
42
  "pytest-cov==7.1.0",
43
43
  "ruff==0.15.12",
44
- "types-xmltodict==v1.0.1.20260408",
44
+ "types-xmltodict==v1.0.1.20260508",
45
45
  ]
46
46
  requirements-dev = [
47
47
  "pre-commit==4.6.0"
@@ -365,3 +365,77 @@ def test_parse_event_xml(input: bytes, expected: dict):
365
365
  def test_unknown_event_operation():
366
366
  """Verify unknown event operation is caught."""
367
367
  assert EventOperation("") == EventOperation.UNKNOWN
368
+
369
+
370
+ @pytest.mark.parametrize(
371
+ ("event_data", "expected"),
372
+ [
373
+ (
374
+ {
375
+ "topic": "tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1Scenario1",
376
+ "source": "",
377
+ "source_idx": "Device1Scenario1",
378
+ "type": "active",
379
+ "value": "0",
380
+ },
381
+ False,
382
+ ),
383
+ (
384
+ {
385
+ "topic": "tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1Scenario1",
386
+ "source": "",
387
+ "source_idx": "Device1Scenario1",
388
+ "type": "classType",
389
+ "value": "human",
390
+ },
391
+ True,
392
+ ),
393
+ (
394
+ {
395
+ "topic": "tns1:Device/tnsaxis:Sensor/PIR",
396
+ "source": "sensor",
397
+ "source_idx": "0",
398
+ "type": "state",
399
+ "value": "0",
400
+ },
401
+ False,
402
+ ),
403
+ (
404
+ {
405
+ "topic": "tns1:AudioSource/tnsaxis:TriggerLevel",
406
+ "source": "channel",
407
+ "source_idx": "1",
408
+ "type": "active",
409
+ "value": "high",
410
+ },
411
+ True,
412
+ ),
413
+ (
414
+ {
415
+ "topic": "tns1:AudioSource/tnsaxis:TriggerLevel",
416
+ "source": "channel",
417
+ "source_idx": "1",
418
+ "type": "active",
419
+ "value": "unknown",
420
+ },
421
+ False,
422
+ ),
423
+ (
424
+ {
425
+ "topic": "tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1Scenario1",
426
+ "source": "",
427
+ "source_idx": "Device1Scenario1",
428
+ "type": "classType",
429
+ "value": " ",
430
+ },
431
+ False,
432
+ ),
433
+ ],
434
+ )
435
+ def test_decode_from_dict_type_aware_is_tripped(
436
+ event_data: dict[str, str], expected: bool
437
+ ) -> None:
438
+ """Verify state handling for binary and semantic event payloads."""
439
+ event = Event.decode(event_data)
440
+
441
+ assert event.is_tripped is expected
@@ -377,6 +377,32 @@ def test_mqtt_event(event_manager: EventManager, subscriber: Mock) -> None:
377
377
  assert event.is_tripped
378
378
 
379
379
 
380
+ def test_mqtt_object_analytics_mixed_data_prefers_active(
381
+ event_manager: EventManager, subscriber: Mock
382
+ ) -> None:
383
+ """Verify object analytics event with semantic and active data uses active state."""
384
+ mqtt_event = {
385
+ "topic": "tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1Scenario1",
386
+ "source": "device",
387
+ "source_idx": "1",
388
+ "type": "active",
389
+ "value": "0",
390
+ }
391
+
392
+ event_manager.handler(mqtt_event)
393
+ assert subscriber.call_count == 1
394
+
395
+ event: Event = subscriber.call_args[0][0]
396
+ assert (
397
+ event.topic
398
+ == "tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1Scenario1"
399
+ )
400
+ assert event.source == "device"
401
+ assert event.id == "1"
402
+ assert event.state == "0"
403
+ assert event.is_tripped is False
404
+
405
+
380
406
  def test_unsupported_event(event_manager: EventManager, subscriber: Mock) -> None:
381
407
  """Verify that unsupported events aren't signalled to subscribers."""
382
408
  event_manager.handler(GLOBAL_SCENE_CHANGE)
@@ -320,6 +320,21 @@ async def test_convert_json_to_event():
320
320
  }
321
321
 
322
322
 
323
+ async def test_convert_json_to_event_prefers_active_value() -> None:
324
+ """Verify conversion prefers binary active state when available."""
325
+ event = mqtt_json_to_event(
326
+ b'{"topic": "onvif:CameraApplicationPlatform/axis:ObjectAnalytics/Device1Scenario1", "message": {"source": {"device": "1"}, "data": {"classTypes": "human", "active": "0"}}}'
327
+ )
328
+
329
+ assert event == {
330
+ "topic": "tns1:CameraApplicationPlatform/tnsaxis:ObjectAnalytics/Device1Scenario1",
331
+ "source": "device",
332
+ "source_idx": "1",
333
+ "type": "active",
334
+ "value": "0",
335
+ }
336
+
337
+
323
338
  def test_mqtt_json_to_event_missing_keys(caplog):
324
339
  """Test mqtt_json_to_event returns None and logs warning if keys are missing."""
325
340
  # Missing 'topic'
@@ -185,6 +185,55 @@ async def test_websocket_stream_receives_data(axis_device):
185
185
  )
186
186
 
187
187
 
188
+ async def test_websocket_stream_prefers_active_value(axis_device):
189
+ """Verify websocket parsing prefers active when multiple data keys are present."""
190
+ callback = MagicMock()
191
+ notify_payload = orjson.dumps(
192
+ {
193
+ "apiVersion": "1.0",
194
+ "method": "events:notify",
195
+ "params": {
196
+ "notification": {
197
+ "timestamp": "2024-01-01T00:00:00Z",
198
+ "topic": "tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1Scenario1",
199
+ "message": {
200
+ "source": {"device": "1"},
201
+ "key": {},
202
+ "data": {"classTypes": "human", "active": "0"},
203
+ },
204
+ }
205
+ },
206
+ }
207
+ )
208
+ ws = MockWebSocket(
209
+ _configure_ok_msg(),
210
+ [
211
+ SimpleNamespace(type=aiohttp.WSMsgType.TEXT, data=notify_payload.decode()),
212
+ SimpleNamespace(type=aiohttp.WSMsgType.CLOSED, data=None),
213
+ ],
214
+ )
215
+
216
+ axis_device.vapix.request = AsyncMock(return_value=b"token123")
217
+ ws_connect = AsyncMock(return_value=ws)
218
+
219
+ with patch.object(axis_device.config.session, "ws_connect", ws_connect):
220
+ client = WebSocketClient(
221
+ axis_device,
222
+ "ws://127.0.0.1:80/vapix/ws-data-stream?sources=events",
223
+ callback,
224
+ )
225
+ await client.start()
226
+ await client._receiver_task
227
+
228
+ assert client.data == {
229
+ "topic": "tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1Scenario1",
230
+ "source": "device",
231
+ "source_idx": "1",
232
+ "type": "active",
233
+ "value": "0",
234
+ }
235
+
236
+
188
237
  async def test_websocket_configure_failure(axis_device):
189
238
  """Verify websocket client reports failed configure."""
190
239
  callback = MagicMock()
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
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
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
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
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
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
File without changes
File without changes
File without changes
File without changes