axis 71__tar.gz → 72__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 (119) hide show
  1. {axis-71 → axis-72}/PKG-INFO +39 -4
  2. {axis-71 → axis-72}/README.md +35 -0
  3. {axis-71 → axis-72}/axis/__main__.py +3 -3
  4. {axis-71 → axis-72}/axis/interfaces/aiohttp_digest.py +0 -2
  5. {axis-71 → axis-72}/axis/interfaces/api_discovery.py +3 -6
  6. {axis-71 → axis-72}/axis/interfaces/api_handler.py +8 -6
  7. {axis-71 → axis-72}/axis/interfaces/applications/application_handler.py +2 -2
  8. {axis-71 → axis-72}/axis/interfaces/applications/applications.py +1 -3
  9. {axis-71 → axis-72}/axis/interfaces/applications/fence_guard.py +1 -3
  10. {axis-71 → axis-72}/axis/interfaces/applications/loitering_guard.py +1 -3
  11. {axis-71 → axis-72}/axis/interfaces/applications/motion_guard.py +1 -3
  12. {axis-71 → axis-72}/axis/interfaces/applications/object_analytics.py +1 -3
  13. {axis-71 → axis-72}/axis/interfaces/applications/vmd4.py +1 -3
  14. {axis-71 → axis-72}/axis/interfaces/basic_device_info.py +2 -6
  15. axis-72/axis/interfaces/event_instances.py +43 -0
  16. {axis-71 → axis-72}/axis/interfaces/event_manager.py +13 -4
  17. {axis-71 → axis-72}/axis/interfaces/light_control.py +28 -37
  18. {axis-71 → axis-72}/axis/interfaces/mqtt.py +2 -37
  19. {axis-71 → axis-72}/axis/interfaces/parameters/param_cgi.py +4 -15
  20. {axis-71 → axis-72}/axis/interfaces/parameters/param_handler.py +2 -2
  21. {axis-71 → axis-72}/axis/interfaces/pir_sensor_configuration.py +3 -11
  22. {axis-71 → axis-72}/axis/interfaces/port_management.py +4 -6
  23. {axis-71 → axis-72}/axis/interfaces/ptz.py +6 -3
  24. {axis-71 → axis-72}/axis/interfaces/pwdgrp_cgi.py +2 -3
  25. {axis-71 → axis-72}/axis/interfaces/stream_profiles.py +2 -6
  26. {axis-71 → axis-72}/axis/interfaces/user_groups.py +3 -3
  27. {axis-71 → axis-72}/axis/interfaces/vapix.py +21 -59
  28. {axis-71 → axis-72}/axis/interfaces/view_areas.py +5 -14
  29. {axis-71 → axis-72}/axis/models/api.py +20 -16
  30. {axis-71 → axis-72}/axis/models/api_discovery.py +5 -4
  31. {axis-71 → axis-72}/axis/models/applications/application.py +10 -9
  32. {axis-71 → axis-72}/axis/models/applications/fence_guard.py +22 -21
  33. {axis-71 → axis-72}/axis/models/applications/loitering_guard.py +22 -21
  34. {axis-71 → axis-72}/axis/models/applications/motion_guard.py +22 -21
  35. {axis-71 → axis-72}/axis/models/applications/object_analytics.py +22 -21
  36. {axis-71 → axis-72}/axis/models/applications/vmd4.py +22 -21
  37. {axis-71 → axis-72}/axis/models/basic_device_info.py +5 -4
  38. {axis-71 → axis-72}/axis/models/configuration.py +1 -3
  39. {axis-71 → axis-72}/axis/models/event.py +13 -43
  40. axis-72/axis/models/event_instance.py +420 -0
  41. {axis-71 → axis-72}/axis/models/light_control.py +175 -151
  42. {axis-71 → axis-72}/axis/models/mqtt.py +84 -56
  43. {axis-71 → axis-72}/axis/models/parameters/brand.py +1 -3
  44. {axis-71 → axis-72}/axis/models/parameters/param_cgi.py +28 -4
  45. {axis-71 → axis-72}/axis/models/parameters/properties.py +1 -3
  46. {axis-71 → axis-72}/axis/models/parameters/ptz.py +1 -3
  47. {axis-71 → axis-72}/axis/models/parameters/stream_profile.py +1 -3
  48. {axis-71 → axis-72}/axis/models/pir_sensor_configuration.py +31 -28
  49. {axis-71 → axis-72}/axis/models/port_cgi.py +3 -2
  50. {axis-71 → axis-72}/axis/models/port_management.py +48 -44
  51. {axis-71 → axis-72}/axis/models/ptz_cgi.py +9 -5
  52. {axis-71 → axis-72}/axis/models/pwdgrp_cgi.py +23 -21
  53. {axis-71 → axis-72}/axis/models/stream_profile.py +26 -25
  54. {axis-71 → axis-72}/axis/models/user_group.py +10 -9
  55. {axis-71 → axis-72}/axis/models/view_area.py +30 -27
  56. {axis-71 → axis-72}/axis/stream_transport.py +0 -2
  57. {axis-71 → axis-72}/axis/websocket.py +0 -2
  58. {axis-71 → axis-72}/axis.egg-info/PKG-INFO +39 -4
  59. {axis-71 → axis-72}/axis.egg-info/SOURCES.txt +2 -0
  60. {axis-71 → axis-72}/axis.egg-info/requires.txt +3 -3
  61. {axis-71 → axis-72}/pyproject.toml +6 -7
  62. {axis-71 → axis-72}/tests/test_api_discovery.py +11 -9
  63. {axis-71 → axis-72}/tests/test_api_handler.py +24 -0
  64. {axis-71 → axis-72}/tests/test_basic_device_info.py +60 -24
  65. axis-72/tests/test_conftest.py +448 -0
  66. {axis-71 → axis-72}/tests/test_event.py +67 -20
  67. axis-72/tests/test_event_instances.py +519 -0
  68. axis-72/tests/test_event_instances_protocol_parity.py +115 -0
  69. {axis-71 → axis-72}/tests/test_event_stream.py +56 -18
  70. {axis-71 → axis-72}/tests/test_http_client_compat.py +3 -7
  71. {axis-71 → axis-72}/tests/test_light_control.py +212 -72
  72. {axis-71 → axis-72}/tests/test_mqtt.py +80 -20
  73. {axis-71 → axis-72}/tests/test_pir_sensor_configuration.py +46 -39
  74. {axis-71 → axis-72}/tests/test_port_cgi.py +39 -5
  75. {axis-71 → axis-72}/tests/test_port_management.py +92 -87
  76. {axis-71 → axis-72}/tests/test_ptz.py +184 -84
  77. {axis-71 → axis-72}/tests/test_pwdgrp_cgi.py +41 -17
  78. axis-72/tests/test_request_response_contracts.py +241 -0
  79. {axis-71 → axis-72}/tests/test_stream_profiles.py +49 -34
  80. {axis-71 → axis-72}/tests/test_vapix.py +179 -37
  81. {axis-71 → axis-72}/tests/test_view_areas.py +62 -32
  82. axis-71/axis/interfaces/event_instances.py +0 -23
  83. axis-71/axis/models/event_instance.py +0 -174
  84. axis-71/tests/test_conftest.py +0 -164
  85. axis-71/tests/test_event_instances.py +0 -271
  86. {axis-71 → axis-72}/LICENSE +0 -0
  87. {axis-71 → axis-72}/axis/__init__.py +0 -0
  88. {axis-71 → axis-72}/axis/device.py +0 -0
  89. {axis-71 → axis-72}/axis/errors.py +0 -0
  90. {axis-71 → axis-72}/axis/interfaces/__init__.py +0 -0
  91. {axis-71 → axis-72}/axis/interfaces/applications/__init__.py +0 -0
  92. {axis-71 → axis-72}/axis/interfaces/parameters/__init__.py +0 -0
  93. {axis-71 → axis-72}/axis/interfaces/parameters/brand.py +0 -0
  94. {axis-71 → axis-72}/axis/interfaces/parameters/image.py +0 -0
  95. {axis-71 → axis-72}/axis/interfaces/parameters/io_port.py +0 -0
  96. {axis-71 → axis-72}/axis/interfaces/parameters/properties.py +0 -0
  97. {axis-71 → axis-72}/axis/interfaces/parameters/ptz.py +0 -0
  98. {axis-71 → axis-72}/axis/interfaces/parameters/stream_profile.py +0 -0
  99. {axis-71 → axis-72}/axis/interfaces/port_cgi.py +0 -0
  100. {axis-71 → axis-72}/axis/models/__init__.py +0 -0
  101. {axis-71 → axis-72}/axis/models/applications/__init__.py +0 -0
  102. {axis-71 → axis-72}/axis/models/parameters/__init__.py +0 -0
  103. {axis-71 → axis-72}/axis/models/parameters/image.py +0 -0
  104. {axis-71 → axis-72}/axis/models/parameters/io_port.py +0 -0
  105. {axis-71 → axis-72}/axis/py.typed +0 -0
  106. {axis-71 → axis-72}/axis/rtsp.py +0 -0
  107. {axis-71 → axis-72}/axis/stream_manager.py +0 -0
  108. {axis-71 → axis-72}/axis.egg-info/dependency_links.txt +0 -0
  109. {axis-71 → axis-72}/axis.egg-info/entry_points.txt +0 -0
  110. {axis-71 → axis-72}/axis.egg-info/top_level.txt +0 -0
  111. {axis-71 → axis-72}/setup.cfg +0 -0
  112. {axis-71 → axis-72}/tests/test_auth_scheme.py +0 -0
  113. {axis-71 → axis-72}/tests/test_configuration.py +0 -0
  114. {axis-71 → axis-72}/tests/test_device.py +0 -0
  115. {axis-71 → axis-72}/tests/test_main_http_client.py +0 -0
  116. {axis-71 → axis-72}/tests/test_rtsp.py +0 -0
  117. {axis-71 → axis-72}/tests/test_stream_manager.py +0 -0
  118. {axis-71 → axis-72}/tests/test_user_groups.py +0 -0
  119. {axis-71 → axis-72}/tests/test_websocket.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: axis
3
- Version: 71
3
+ Version: 72
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
@@ -28,13 +28,13 @@ Requires-Dist: orjson==3.11.9; extra == "requirements"
28
28
  Requires-Dist: packaging==26.2; extra == "requirements"
29
29
  Requires-Dist: xmltodict==1.0.4; extra == "requirements"
30
30
  Provides-Extra: requirements-test
31
- Requires-Dist: mypy==2.0.0; extra == "requirements-test"
31
+ Requires-Dist: mypy==2.1.0; extra == "requirements-test"
32
32
  Requires-Dist: pytest==9.0.3; extra == "requirements-test"
33
33
  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
- Requires-Dist: ruff==0.15.12; extra == "requirements-test"
37
- Requires-Dist: types-xmltodict==v1.0.1.20260508; extra == "requirements-test"
36
+ Requires-Dist: ruff==0.15.13; extra == "requirements-test"
37
+ Requires-Dist: types-xmltodict==v1.0.1.20260518; 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
@@ -89,7 +89,42 @@ Vapix initialization is phase-based and driven by handler metadata:
89
89
 
90
90
  Handlers declare phase membership through `handler_groups` and may customize phase eligibility through `should_initialize_in_group`.
91
91
 
92
+ ## Request and response typing
93
+
94
+ `Vapix.api_request()` is the single typed request entrypoint.
95
+
96
+ - Request models declare their decode contract with `ApiRequest[ResponseT]`.
97
+ - Every `ApiRequest` subclass must set `response_type` explicitly.
98
+ - Decoded/read requests use their concrete response model as `response_type`.
99
+ - Write-style requests use `BytesResponse` as `response_type`.
100
+
101
+ Examples:
102
+
103
+ ```python
104
+ @dataclass
105
+ class ListApisRequest(ApiRequest[GetAllApisResponse]):
106
+ response_type = GetAllApisResponse
107
+
108
+
109
+ @dataclass
110
+ class SetPortsRequest(ApiRequest[ApiResponse[bytes]]):
111
+ response_type = BytesResponse
112
+ ```
113
+
114
+ Handler methods may unwrap `.data` when they intentionally preserve a bytes-returning boundary.
115
+
92
116
  Example fallback policy:
93
117
 
94
118
  - `LightHandler` participates in both `API_DISCOVERY` and `PARAM_CGI_FALLBACK`.
95
119
  - In `PARAM_CGI_FALLBACK`, it initializes only when not listed in API discovery and listed in parameters.
120
+
121
+ ## Event Instance Model Notes
122
+
123
+ `EventInstance` keeps `name` as the raw device-provided `NiceName` value.
124
+
125
+ `EventInstance.source` and `EventInstance.data` are typed containers (`EventInstanceSource` and `EventInstanceData`) built from `SimpleItemInstance` payloads.
126
+
127
+ For compatibility with integrations that still need the historical raw payload shape, `EventInstance` also exposes:
128
+
129
+ - `raw_source`: returns `{}` or a dict or a list of dicts.
130
+ - `raw_data`: returns `{}` or a dict or a list of dicts.
@@ -48,7 +48,42 @@ Vapix initialization is phase-based and driven by handler metadata:
48
48
 
49
49
  Handlers declare phase membership through `handler_groups` and may customize phase eligibility through `should_initialize_in_group`.
50
50
 
51
+ ## Request and response typing
52
+
53
+ `Vapix.api_request()` is the single typed request entrypoint.
54
+
55
+ - Request models declare their decode contract with `ApiRequest[ResponseT]`.
56
+ - Every `ApiRequest` subclass must set `response_type` explicitly.
57
+ - Decoded/read requests use their concrete response model as `response_type`.
58
+ - Write-style requests use `BytesResponse` as `response_type`.
59
+
60
+ Examples:
61
+
62
+ ```python
63
+ @dataclass
64
+ class ListApisRequest(ApiRequest[GetAllApisResponse]):
65
+ response_type = GetAllApisResponse
66
+
67
+
68
+ @dataclass
69
+ class SetPortsRequest(ApiRequest[ApiResponse[bytes]]):
70
+ response_type = BytesResponse
71
+ ```
72
+
73
+ Handler methods may unwrap `.data` when they intentionally preserve a bytes-returning boundary.
74
+
51
75
  Example fallback policy:
52
76
 
53
77
  - `LightHandler` participates in both `API_DISCOVERY` and `PARAM_CGI_FALLBACK`.
54
78
  - In `PARAM_CGI_FALLBACK`, it initializes only when not listed in API discovery and listed in parameters.
79
+
80
+ ## Event Instance Model Notes
81
+
82
+ `EventInstance` keeps `name` as the raw device-provided `NiceName` value.
83
+
84
+ `EventInstance.source` and `EventInstance.data` are typed containers (`EventInstanceSource` and `EventInstanceData`) built from `SimpleItemInstance` payloads.
85
+
86
+ For compatibility with integrations that still need the historical raw payload shape, `EventInstance` also exposes:
87
+
88
+ - `raw_source`: returns `{}` or a dict or a list of dicts.
89
+ - `raw_data`: returns `{}` or a dict or a list of dicts.
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
17
17
  LOGGER = logging.getLogger(__name__)
18
18
 
19
19
 
20
- def event_handler(event: "Event") -> None:
20
+ def event_handler(event: Event) -> None:
21
21
  """Receive and print events from RTSP stream."""
22
22
  LOGGER.info(event)
23
23
 
@@ -61,8 +61,8 @@ async def axis_device(
61
61
  "Connected to device at %s but not registered or user not admin.", host
62
62
  )
63
63
 
64
- except (TimeoutError, axis.RequestError):
65
- LOGGER.error("Error connecting to the Axis device at %s", host)
64
+ except (TimeoutError, axis.RequestError) as err:
65
+ LOGGER.error("Error connecting to the Axis device at %s: %s", host, err)
66
66
 
67
67
  except axis.AxisException:
68
68
  LOGGER.exception("Unknown Axis communication error occurred")
@@ -4,8 +4,6 @@ Implements library-managed RFC 2617 digest authentication for aiohttp requests
4
4
  to handle special characters in request parameters that break middleware-based auth.
5
5
  """
6
6
 
7
- from __future__ import annotations
8
-
9
7
  import hashlib
10
8
  import logging
11
9
  import re
@@ -8,9 +8,7 @@ from ..models.api_discovery import (
8
8
  API_VERSION,
9
9
  Api,
10
10
  ApiId,
11
- GetAllApisResponse,
12
11
  GetSupportedVersionsRequest,
13
- GetSupportedVersionsResponse,
14
12
  ListApisRequest,
15
13
  )
16
14
  from .api_handler import ApiHandler
@@ -33,11 +31,10 @@ class ApiDiscoveryHandler(ApiHandler[Api]):
33
31
 
34
32
  async def get_api_list(self) -> dict[str, Api]:
35
33
  """List all APIs registered on API Discovery service."""
36
- bytes_data = await self.vapix.api_request(ListApisRequest())
37
- return GetAllApisResponse.decode(bytes_data).data
34
+ response = await self.vapix.api_request(ListApisRequest())
35
+ return response.data
38
36
 
39
37
  async def get_supported_versions(self) -> list[str]:
40
38
  """List supported API versions."""
41
- bytes_data = await self.vapix.api_request(GetSupportedVersionsRequest())
42
- response = GetSupportedVersionsResponse.decode(bytes_data)
39
+ response = await self.vapix.api_request(GetSupportedVersionsRequest())
43
40
  return response.data
@@ -9,7 +9,7 @@ from collections.abc import (
9
9
  ValuesView,
10
10
  )
11
11
  import enum
12
- from typing import TYPE_CHECKING, Generic, final
12
+ from typing import TYPE_CHECKING, final
13
13
 
14
14
  from ..errors import Forbidden, PathNotFound, Unauthorized
15
15
 
@@ -17,11 +17,11 @@ if TYPE_CHECKING:
17
17
  from ..models.api_discovery import ApiId
18
18
  from .vapix import Vapix
19
19
 
20
- from ..models.api import ApiItemT
20
+ from ..models.api import ApiItem
21
21
 
22
- CallbackType = Callable[[str], None]
23
- SubscriptionType = CallbackType
24
- UnsubscribeType = Callable[[], None]
22
+ type CallbackType = Callable[[str], None]
23
+ type SubscriptionType = CallbackType
24
+ type UnsubscribeType = Callable[[], None]
25
25
 
26
26
  ID_FILTER_ALL = "*"
27
27
 
@@ -62,6 +62,8 @@ class SubscriptionHandler:
62
62
  _id_filter = (ID_FILTER_ALL,)
63
63
  elif isinstance(id_filter, str):
64
64
  _id_filter = (id_filter,)
65
+ else:
66
+ _id_filter = id_filter
65
67
 
66
68
  for obj_id in _id_filter:
67
69
  if obj_id not in self._subscribers:
@@ -79,7 +81,7 @@ class SubscriptionHandler:
79
81
  return unsubscribe
80
82
 
81
83
 
82
- class ApiHandler(SubscriptionHandler, Generic[ApiItemT]):
84
+ class ApiHandler[ApiItemT: ApiItem](SubscriptionHandler):
83
85
  """Base class for a map of API Items."""
84
86
 
85
87
  api_id: ApiId | None = None
@@ -2,12 +2,12 @@
2
2
 
3
3
  from abc import abstractmethod
4
4
 
5
- from ...models.api import ApiItemT
5
+ from ...models.api import ApiItem
6
6
  from ...models.applications.application import ApplicationName, ApplicationStatus
7
7
  from ..api_handler import ApiHandler, HandlerGroup
8
8
 
9
9
 
10
- class ApplicationHandler(ApiHandler[ApiItemT]):
10
+ class ApplicationHandler[ApiItemT: ApiItem](ApiHandler[ApiItemT]):
11
11
  """Generic application handler."""
12
12
 
13
13
  app_name: ApplicationName
@@ -9,7 +9,6 @@ from packaging import version
9
9
  from ...models.applications.application import (
10
10
  Application,
11
11
  ListApplicationsRequest,
12
- ListApplicationsResponse,
13
12
  )
14
13
  from ..api_handler import ApiHandler
15
14
 
@@ -36,6 +35,5 @@ class ApplicationsHandler(ApiHandler[Application]):
36
35
 
37
36
  async def list_applications(self) -> dict[str, Application]:
38
37
  """List all APIs registered on API Discovery service."""
39
- bytes_data = await self.vapix.api_request(ListApplicationsRequest())
40
- response = ListApplicationsResponse.decode(bytes_data)
38
+ response = await self.vapix.api_request(ListApplicationsRequest())
41
39
  return response.data
@@ -10,7 +10,6 @@ from ...models.applications.application import ApplicationName
10
10
  from ...models.applications.fence_guard import (
11
11
  Configuration,
12
12
  GetConfigurationRequest,
13
- GetConfigurationResponse,
14
13
  )
15
14
  from .application_handler import ApplicationHandler
16
15
 
@@ -22,6 +21,5 @@ class FenceGuardHandler(ApplicationHandler[Configuration]):
22
21
 
23
22
  async def get_configuration(self) -> Configuration:
24
23
  """Get configuration of VMD4 application."""
25
- bytes_data = await self.vapix.api_request(GetConfigurationRequest())
26
- response = GetConfigurationResponse.decode(bytes_data)
24
+ response = await self.vapix.api_request(GetConfigurationRequest())
27
25
  return response.data
@@ -8,7 +8,6 @@ from ...models.applications.application import ApplicationName
8
8
  from ...models.applications.loitering_guard import (
9
9
  Configuration,
10
10
  GetConfigurationRequest,
11
- GetConfigurationResponse,
12
11
  )
13
12
  from .application_handler import ApplicationHandler
14
13
 
@@ -20,6 +19,5 @@ class LoiteringGuardHandler(ApplicationHandler[Configuration]):
20
19
 
21
20
  async def get_configuration(self) -> Configuration:
22
21
  """Get configuration of VMD4 application."""
23
- bytes_data = await self.vapix.api_request(GetConfigurationRequest())
24
- response = GetConfigurationResponse.decode(bytes_data)
22
+ response = await self.vapix.api_request(GetConfigurationRequest())
25
23
  return response.data
@@ -9,7 +9,6 @@ from ...models.applications.application import ApplicationName
9
9
  from ...models.applications.motion_guard import (
10
10
  Configuration,
11
11
  GetConfigurationRequest,
12
- GetConfigurationResponse,
13
12
  )
14
13
  from .application_handler import ApplicationHandler
15
14
 
@@ -21,6 +20,5 @@ class MotionGuardHandler(ApplicationHandler[Configuration]):
21
20
 
22
21
  async def get_configuration(self) -> Configuration:
23
22
  """Get configuration of VMD4 application."""
24
- bytes_data = await self.vapix.api_request(GetConfigurationRequest())
25
- response = GetConfigurationResponse.decode(bytes_data)
23
+ response = await self.vapix.api_request(GetConfigurationRequest())
26
24
  return response.data
@@ -7,7 +7,6 @@ from ...models.applications.application import ApplicationName
7
7
  from ...models.applications.object_analytics import (
8
8
  Configuration,
9
9
  GetConfigurationRequest,
10
- GetConfigurationResponse,
11
10
  )
12
11
  from .application_handler import ApplicationHandler
13
12
 
@@ -19,6 +18,5 @@ class ObjectAnalyticsHandler(ApplicationHandler[Configuration]):
19
18
 
20
19
  async def get_configuration(self) -> Configuration:
21
20
  """Get configuration of object analytics application."""
22
- bytes_data = await self.vapix.api_request(GetConfigurationRequest())
23
- response = GetConfigurationResponse.decode(bytes_data)
21
+ response = await self.vapix.api_request(GetConfigurationRequest())
24
22
  return response.data
@@ -4,7 +4,6 @@ from ...models.applications.application import ApplicationName
4
4
  from ...models.applications.vmd4 import (
5
5
  Configuration,
6
6
  GetConfigurationRequest,
7
- GetConfigurationResponse,
8
7
  )
9
8
  from .application_handler import ApplicationHandler
10
9
 
@@ -16,6 +15,5 @@ class Vmd4Handler(ApplicationHandler[Configuration]):
16
15
 
17
16
  async def get_configuration(self) -> Configuration:
18
17
  """Get configuration of VMD4 application."""
19
- bytes_data = await self.vapix.api_request(GetConfigurationRequest())
20
- response = GetConfigurationResponse.decode(bytes_data)
18
+ response = await self.vapix.api_request(GetConfigurationRequest())
21
19
  return response.data
@@ -10,9 +10,7 @@ from ..models.basic_device_info import (
10
10
  API_VERSION,
11
11
  DeviceInformation,
12
12
  GetAllPropertiesRequest,
13
- GetAllPropertiesResponse,
14
13
  GetSupportedVersionsRequest,
15
- GetSupportedVersionsResponse,
16
14
  )
17
15
  from .api_handler import ApiHandler, HandlerGroup
18
16
 
@@ -30,14 +28,12 @@ class BasicDeviceInfoHandler(ApiHandler[DeviceInformation]):
30
28
 
31
29
  async def get_all_properties(self) -> dict[str, DeviceInformation]:
32
30
  """List all properties of basic device info."""
33
- bytes_data = await self.vapix.api_request(
31
+ response = await self.vapix.api_request(
34
32
  GetAllPropertiesRequest(self.api_version)
35
33
  )
36
- response = GetAllPropertiesResponse.decode(bytes_data)
37
34
  return {"0": response.data}
38
35
 
39
36
  async def get_supported_versions(self) -> list[str]:
40
37
  """List supported API versions."""
41
- bytes_data = await self.vapix.api_request(GetSupportedVersionsRequest())
42
- response = GetSupportedVersionsResponse.decode(bytes_data)
38
+ response = await self.vapix.api_request(GetSupportedVersionsRequest())
43
39
  return response.data
@@ -0,0 +1,43 @@
1
+ """Event service and action service APIs available in Axis network device."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from ..models.event_instance import (
6
+ EventInstance,
7
+ ListEventInstancesRequest,
8
+ )
9
+ from .api_handler import ApiHandler
10
+ from .event_manager import BLACK_LISTED_TOPICS
11
+
12
+ if TYPE_CHECKING:
13
+ from ..models.event import Event
14
+
15
+
16
+ class EventInstanceHandler(ApiHandler[EventInstance]):
17
+ """Event instances for Axis devices."""
18
+
19
+ async def _api_request(self) -> dict[str, EventInstance]:
20
+ """Get default data of API discovery."""
21
+ return await self.get_event_instances()
22
+
23
+ async def get_event_instances(self) -> dict[str, EventInstance]:
24
+ """List all event instances."""
25
+ response = await self.vapix.api_request(ListEventInstancesRequest())
26
+ return response.data
27
+
28
+ def get_expected_events_per_topic(
29
+ self,
30
+ include_internal_topics: bool = False,
31
+ ) -> dict[str, list[Event]]:
32
+ """Return expected startup events grouped by topic.
33
+
34
+ Event instances are the protocol-agnostic bootstrap source for startup
35
+ predeclaration. Returned events are synthesized from schema data and represent
36
+ expected event identity/state (operation=Initialized), not live stream updates.
37
+ """
38
+ grouped: dict[str, list[Event]] = {}
39
+ for item in self.values():
40
+ if not include_internal_topics and item.topic in BLACK_LISTED_TOPICS:
41
+ continue
42
+ grouped[item.topic] = item.to_events()
43
+ return grouped
@@ -6,17 +6,19 @@ from typing import Any
6
6
 
7
7
  from ..models.event import Event, EventOperation, EventTopic
8
8
 
9
- SubscriptionCallback = Callable[[Event], None]
10
- SubscriptionType = tuple[
9
+ type SubscriptionCallback = Callable[[Event], None]
10
+ type SubscriptionType = tuple[
11
11
  SubscriptionCallback,
12
12
  tuple[EventTopic, ...] | None,
13
13
  tuple[EventOperation, ...] | None,
14
14
  ]
15
- UnsubscribeType = Callable[[], None]
15
+ type UnsubscribeType = Callable[[], None]
16
16
 
17
17
  ID_FILTER_ALL = "*"
18
18
 
19
19
  BLACK_LISTED_TOPICS = [
20
+ "tns1:RuleEngine/tnsaxis:VideoMotionDetection/timer",
21
+ "tns1:RuleEngine/tnsaxis:VMD3/timer",
20
22
  "tnsaxis:CameraApplicationPlatform/VMD/xinternal_data",
21
23
  "tnsaxis:CameraApplicationPlatform/ObjectAnalytics/xinternal_data",
22
24
  ]
@@ -29,6 +31,7 @@ class EventManager:
29
31
  def __init__(self) -> None:
30
32
  """Ready information about events."""
31
33
  self._known_topics: set[str] = set()
34
+ self._unsupported_topics: set[str] = set()
32
35
  self._subscribers: dict[str, list[SubscriptionType]] = {ID_FILTER_ALL: []}
33
36
 
34
37
  def handler(self, data: bytes | dict[str, Any]) -> None:
@@ -37,7 +40,13 @@ class EventManager:
37
40
  if LOGGER.isEnabledFor(logging.DEBUG):
38
41
  LOGGER.debug(event)
39
42
 
40
- if event.topic_base == EventTopic.UNKNOWN or event.topic in BLACK_LISTED_TOPICS:
43
+ if event.topic_base == EventTopic.UNKNOWN:
44
+ if event.topic not in self._unsupported_topics:
45
+ LOGGER.warning("Ignoring unsupported event topic %s", event.topic)
46
+ self._unsupported_topics.add(event.topic)
47
+ return
48
+
49
+ if event.topic in BLACK_LISTED_TOPICS:
41
50
  return
42
51
 
43
52
  known = (unique_topic := f"{event.topic}_{event.id}") not in self._known_topics
@@ -12,29 +12,18 @@ from ..models.light_control import (
12
12
  DisableLightRequest,
13
13
  EnableLightRequest,
14
14
  GetCurrentAngleOfIlluminationRequest,
15
- GetCurrentAngleOfIlluminationResponse,
16
15
  GetCurrentIntensityRequest,
17
- GetCurrentIntensityResponse,
18
16
  GetIndividualIntensityRequest,
19
- GetIndividualIntensityResponse,
20
17
  GetLightInformationRequest,
21
- GetLightInformationResponse,
22
18
  GetLightStatusRequest,
23
- GetLightStatusResponse,
24
19
  GetLightSynchronizeDayNightModeRequest,
25
20
  GetLightSynchronizeDayNightModeResponse,
26
21
  GetManualAngleOfIlluminationRequest,
27
- GetManualAngleOfIlluminationResponse,
28
22
  GetManualIntensityRequest,
29
- GetManualIntensityResponse,
30
23
  GetServiceCapabilitiesRequest,
31
- GetServiceCapabilitiesResponse,
32
24
  GetSupportedVersionsRequest,
33
- GetSupportedVersionsResponse,
34
25
  GetValidAngleOfIlluminationRequest,
35
- GetValidAngleOfIlluminationResponse,
36
26
  GetValidIntensityRequest,
37
- GetValidIntensityResponse,
38
27
  LightInformation,
39
28
  Range,
40
29
  ServiceCapabilities,
@@ -77,17 +66,17 @@ class LightHandler(ApiHandler[LightInformation]):
77
66
 
78
67
  async def get_light_information(self) -> dict[str, LightInformation]:
79
68
  """List the light control information."""
80
- bytes_data = await self.vapix.api_request(
69
+ response = await self.vapix.api_request(
81
70
  GetLightInformationRequest(api_version=self.api_version)
82
71
  )
83
- return GetLightInformationResponse.decode(bytes_data).data
72
+ return response.data
84
73
 
85
74
  async def get_service_capabilities(self) -> ServiceCapabilities:
86
75
  """List the light control information."""
87
- bytes_data = await self.vapix.api_request(
76
+ response = await self.vapix.api_request(
88
77
  GetServiceCapabilitiesRequest(api_version=self.api_version)
89
78
  )
90
- return GetServiceCapabilitiesResponse.decode(bytes_data).data
79
+ return response.data
91
80
 
92
81
  async def activate_light(self, light_id: str) -> None:
93
82
  """Activate the light."""
@@ -115,10 +104,10 @@ class LightHandler(ApiHandler[LightInformation]):
115
104
 
116
105
  async def get_light_status(self, light_id: str) -> bool:
117
106
  """Get light status if its on or off."""
118
- bytes_data = await self.vapix.api_request(
107
+ response = await self.vapix.api_request(
119
108
  GetLightStatusRequest(api_version=self.api_version, light_id=light_id)
120
109
  )
121
- return GetLightStatusResponse.decode(bytes_data).data
110
+ return response.data
122
111
 
123
112
  async def set_automatic_intensity_mode(self, light_id: str, enabled: bool) -> None:
124
113
  """Enable the automatic light intensity control."""
@@ -132,10 +121,10 @@ class LightHandler(ApiHandler[LightInformation]):
132
121
 
133
122
  async def get_valid_intensity(self, light_id: str) -> Range:
134
123
  """Get valid intensity range for light."""
135
- bytes_data = await self.vapix.api_request(
124
+ response = await self.vapix.api_request(
136
125
  GetValidIntensityRequest(api_version=self.api_version, light_id=light_id)
137
126
  )
138
- return GetValidIntensityResponse.decode(bytes_data).data
127
+ return response.data
139
128
 
140
129
  async def set_manual_intensity(self, light_id: str, intensity: int) -> None:
141
130
  """Manually sets the intensity."""
@@ -149,10 +138,10 @@ class LightHandler(ApiHandler[LightInformation]):
149
138
 
150
139
  async def get_manual_intensity(self, light_id: str) -> int:
151
140
  """Enable the automatic light intensity control."""
152
- bytes_data = await self.vapix.api_request(
141
+ response = await self.vapix.api_request(
153
142
  GetManualIntensityRequest(api_version=self.api_version, light_id=light_id)
154
143
  )
155
- return GetManualIntensityResponse.decode(bytes_data).data
144
+ return response.data
156
145
 
157
146
  async def set_individual_intensity(
158
147
  self, light_id: str, led_id: int, intensity: int
@@ -169,21 +158,21 @@ class LightHandler(ApiHandler[LightInformation]):
169
158
 
170
159
  async def get_individual_intensity(self, light_id: str, led_id: int) -> int:
171
160
  """Receives the intensity from the setIndividualIntensity request."""
172
- bytes_data = await self.vapix.api_request(
161
+ response = await self.vapix.api_request(
173
162
  GetIndividualIntensityRequest(
174
163
  api_version=self.api_version,
175
164
  light_id=light_id,
176
165
  led_id=led_id,
177
166
  )
178
167
  )
179
- return GetIndividualIntensityResponse.decode(bytes_data).data
168
+ return response.data
180
169
 
181
170
  async def get_current_intensity(self, light_id: str) -> int:
182
171
  """Receives the intensity from the setIndividualIntensity request."""
183
- bytes_data = await self.vapix.api_request(
172
+ response = await self.vapix.api_request(
184
173
  GetCurrentIntensityRequest(api_version=self.api_version, light_id=light_id)
185
174
  )
186
- return GetCurrentIntensityResponse.decode(bytes_data).data
175
+ return response.data
187
176
 
188
177
  async def set_automatic_angle_of_illumination_mode(
189
178
  self, light_id: str, enabled: bool
@@ -203,12 +192,12 @@ class LightHandler(ApiHandler[LightInformation]):
203
192
 
204
193
  async def get_valid_angle_of_illumination(self, light_id: str) -> list[Range]:
205
194
  """List the valid angle of illumination values."""
206
- bytes_data = await self.vapix.api_request(
195
+ response = await self.vapix.api_request(
207
196
  GetValidAngleOfIlluminationRequest(
208
197
  api_version=self.api_version, light_id=light_id
209
198
  )
210
199
  )
211
- return GetValidAngleOfIlluminationResponse.decode(bytes_data).data
200
+ return response.data
212
201
 
213
202
  async def set_manual_angle_of_illumination(
214
203
  self, light_id: str, angle_of_illumination: int
@@ -228,21 +217,21 @@ class LightHandler(ApiHandler[LightInformation]):
228
217
 
229
218
  async def get_manual_angle_of_illumination(self, light_id: str) -> int:
230
219
  """Get the angle of illumination."""
231
- bytes_data = await self.vapix.api_request(
220
+ response = await self.vapix.api_request(
232
221
  GetManualAngleOfIlluminationRequest(
233
222
  api_version=self.api_version, light_id=light_id
234
223
  )
235
224
  )
236
- return GetManualAngleOfIlluminationResponse.decode(bytes_data).data
225
+ return response.data
237
226
 
238
227
  async def get_current_angle_of_illumination(self, light_id: str) -> int:
239
228
  """Receive the current angle of illumination."""
240
- bytes_data = await self.vapix.api_request(
229
+ response = await self.vapix.api_request(
241
230
  GetCurrentAngleOfIlluminationRequest(
242
231
  api_version=self.api_version, light_id=light_id
243
232
  )
244
233
  )
245
- return GetCurrentAngleOfIlluminationResponse.decode(bytes_data).data
234
+ return response.data
246
235
 
247
236
  async def set_light_synchronization_day_night_mode(
248
237
  self, light_id: str, enabled: bool
@@ -258,14 +247,16 @@ class LightHandler(ApiHandler[LightInformation]):
258
247
 
259
248
  async def get_light_synchronization_day_night_mode(self, light_id: str) -> bool:
260
249
  """Check if the automatic synchronization is enabled with the day/night mode."""
261
- bytes_data = await self.vapix.api_request(
262
- GetLightSynchronizeDayNightModeRequest(
263
- api_version=self.api_version, light_id=light_id
250
+ response: GetLightSynchronizeDayNightModeResponse = (
251
+ await self.vapix.api_request(
252
+ GetLightSynchronizeDayNightModeRequest(
253
+ api_version=self.api_version, light_id=light_id
254
+ )
264
255
  )
265
256
  )
266
- return GetLightSynchronizeDayNightModeResponse.decode(bytes_data).data
257
+ return response.data
267
258
 
268
259
  async def get_supported_versions(self) -> list[str]:
269
260
  """List supported API versions."""
270
- bytes_data = await self.vapix.api_request(GetSupportedVersionsRequest())
271
- return GetSupportedVersionsResponse.decode(bytes_data).data
261
+ response = await self.vapix.api_request(GetSupportedVersionsRequest())
262
+ return response.data