axis 67__tar.gz → 68__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-67 → axis-68}/PKG-INFO +59 -6
  2. axis-68/README.md +54 -0
  3. {axis-67 → axis-68}/axis/__main__.py +21 -0
  4. {axis-67 → axis-68}/axis/interfaces/api_handler.py +50 -9
  5. {axis-67 → axis-68}/axis/interfaces/applications/application_handler.py +2 -1
  6. {axis-67 → axis-68}/axis/interfaces/basic_device_info.py +3 -3
  7. {axis-67 → axis-68}/axis/interfaces/light_control.py +36 -34
  8. {axis-67 → axis-68}/axis/interfaces/mqtt.py +21 -15
  9. {axis-67 → axis-68}/axis/interfaces/pir_sensor_configuration.py +8 -9
  10. {axis-67 → axis-68}/axis/interfaces/port_management.py +15 -4
  11. {axis-67 → axis-68}/axis/interfaces/stream_profiles.py +3 -3
  12. {axis-67 → axis-68}/axis/interfaces/vapix.py +34 -23
  13. {axis-67 → axis-68}/axis/interfaces/view_areas.py +7 -7
  14. {axis-67 → axis-68}/axis/models/configuration.py +2 -0
  15. {axis-67 → axis-68}/axis/rtsp.py +5 -0
  16. {axis-67 → axis-68}/axis/stream_manager.py +75 -15
  17. axis-68/axis/stream_transport.py +34 -0
  18. axis-68/axis/websocket.py +357 -0
  19. {axis-67 → axis-68}/axis.egg-info/PKG-INFO +59 -6
  20. {axis-67 → axis-68}/axis.egg-info/SOURCES.txt +4 -1
  21. {axis-67 → axis-68}/axis.egg-info/requires.txt +5 -5
  22. {axis-67 → axis-68}/pyproject.toml +12 -6
  23. axis-68/tests/test_api_handler.py +178 -0
  24. {axis-67 → axis-68}/tests/test_basic_device_info.py +1 -1
  25. {axis-67 → axis-68}/tests/test_configuration.py +19 -0
  26. {axis-67 → axis-68}/tests/test_light_control.py +47 -47
  27. {axis-67 → axis-68}/tests/test_main_http_client.py +18 -1
  28. {axis-67 → axis-68}/tests/test_mqtt.py +36 -0
  29. {axis-67 → axis-68}/tests/test_rtsp.py +9 -0
  30. {axis-67 → axis-68}/tests/test_stream_manager.py +141 -2
  31. {axis-67 → axis-68}/tests/test_vapix.py +113 -1
  32. axis-68/tests/test_websocket.py +540 -0
  33. axis-67/README.md +0 -1
  34. axis-67/tests/test_api_handler.py +0 -83
  35. {axis-67 → axis-68}/LICENSE +0 -0
  36. {axis-67 → axis-68}/axis/__init__.py +0 -0
  37. {axis-67 → axis-68}/axis/device.py +0 -0
  38. {axis-67 → axis-68}/axis/errors.py +0 -0
  39. {axis-67 → axis-68}/axis/interfaces/__init__.py +0 -0
  40. {axis-67 → axis-68}/axis/interfaces/api_discovery.py +0 -0
  41. {axis-67 → axis-68}/axis/interfaces/applications/__init__.py +0 -0
  42. {axis-67 → axis-68}/axis/interfaces/applications/applications.py +0 -0
  43. {axis-67 → axis-68}/axis/interfaces/applications/fence_guard.py +0 -0
  44. {axis-67 → axis-68}/axis/interfaces/applications/loitering_guard.py +0 -0
  45. {axis-67 → axis-68}/axis/interfaces/applications/motion_guard.py +0 -0
  46. {axis-67 → axis-68}/axis/interfaces/applications/object_analytics.py +0 -0
  47. {axis-67 → axis-68}/axis/interfaces/applications/vmd4.py +0 -0
  48. {axis-67 → axis-68}/axis/interfaces/event_instances.py +0 -0
  49. {axis-67 → axis-68}/axis/interfaces/event_manager.py +0 -0
  50. {axis-67 → axis-68}/axis/interfaces/parameters/__init__.py +0 -0
  51. {axis-67 → axis-68}/axis/interfaces/parameters/brand.py +0 -0
  52. {axis-67 → axis-68}/axis/interfaces/parameters/image.py +0 -0
  53. {axis-67 → axis-68}/axis/interfaces/parameters/io_port.py +0 -0
  54. {axis-67 → axis-68}/axis/interfaces/parameters/param_cgi.py +0 -0
  55. {axis-67 → axis-68}/axis/interfaces/parameters/param_handler.py +0 -0
  56. {axis-67 → axis-68}/axis/interfaces/parameters/properties.py +0 -0
  57. {axis-67 → axis-68}/axis/interfaces/parameters/ptz.py +0 -0
  58. {axis-67 → axis-68}/axis/interfaces/parameters/stream_profile.py +0 -0
  59. {axis-67 → axis-68}/axis/interfaces/port_cgi.py +0 -0
  60. {axis-67 → axis-68}/axis/interfaces/ptz.py +0 -0
  61. {axis-67 → axis-68}/axis/interfaces/pwdgrp_cgi.py +0 -0
  62. {axis-67 → axis-68}/axis/interfaces/user_groups.py +0 -0
  63. {axis-67 → axis-68}/axis/models/__init__.py +0 -0
  64. {axis-67 → axis-68}/axis/models/api.py +0 -0
  65. {axis-67 → axis-68}/axis/models/api_discovery.py +0 -0
  66. {axis-67 → axis-68}/axis/models/applications/__init__.py +0 -0
  67. {axis-67 → axis-68}/axis/models/applications/application.py +0 -0
  68. {axis-67 → axis-68}/axis/models/applications/fence_guard.py +0 -0
  69. {axis-67 → axis-68}/axis/models/applications/loitering_guard.py +0 -0
  70. {axis-67 → axis-68}/axis/models/applications/motion_guard.py +0 -0
  71. {axis-67 → axis-68}/axis/models/applications/object_analytics.py +0 -0
  72. {axis-67 → axis-68}/axis/models/applications/vmd4.py +0 -0
  73. {axis-67 → axis-68}/axis/models/basic_device_info.py +0 -0
  74. {axis-67 → axis-68}/axis/models/event.py +0 -0
  75. {axis-67 → axis-68}/axis/models/event_instance.py +0 -0
  76. {axis-67 → axis-68}/axis/models/light_control.py +0 -0
  77. {axis-67 → axis-68}/axis/models/mqtt.py +0 -0
  78. {axis-67 → axis-68}/axis/models/parameters/__init__.py +0 -0
  79. {axis-67 → axis-68}/axis/models/parameters/brand.py +0 -0
  80. {axis-67 → axis-68}/axis/models/parameters/image.py +0 -0
  81. {axis-67 → axis-68}/axis/models/parameters/io_port.py +0 -0
  82. {axis-67 → axis-68}/axis/models/parameters/param_cgi.py +0 -0
  83. {axis-67 → axis-68}/axis/models/parameters/properties.py +0 -0
  84. {axis-67 → axis-68}/axis/models/parameters/ptz.py +0 -0
  85. {axis-67 → axis-68}/axis/models/parameters/stream_profile.py +0 -0
  86. {axis-67 → axis-68}/axis/models/pir_sensor_configuration.py +0 -0
  87. {axis-67 → axis-68}/axis/models/port_cgi.py +0 -0
  88. {axis-67 → axis-68}/axis/models/port_management.py +0 -0
  89. {axis-67 → axis-68}/axis/models/ptz_cgi.py +0 -0
  90. {axis-67 → axis-68}/axis/models/pwdgrp_cgi.py +0 -0
  91. {axis-67 → axis-68}/axis/models/stream_profile.py +0 -0
  92. {axis-67 → axis-68}/axis/models/user_group.py +0 -0
  93. {axis-67 → axis-68}/axis/models/view_area.py +0 -0
  94. {axis-67 → axis-68}/axis/py.typed +0 -0
  95. {axis-67 → axis-68}/axis.egg-info/dependency_links.txt +0 -0
  96. {axis-67 → axis-68}/axis.egg-info/entry_points.txt +0 -0
  97. {axis-67 → axis-68}/axis.egg-info/top_level.txt +0 -0
  98. {axis-67 → axis-68}/setup.cfg +0 -0
  99. {axis-67 → axis-68}/tests/test_api_discovery.py +0 -0
  100. {axis-67 → axis-68}/tests/test_auth_scheme.py +0 -0
  101. {axis-67 → axis-68}/tests/test_device.py +0 -0
  102. {axis-67 → axis-68}/tests/test_event.py +0 -0
  103. {axis-67 → axis-68}/tests/test_event_instances.py +0 -0
  104. {axis-67 → axis-68}/tests/test_event_stream.py +0 -0
  105. {axis-67 → axis-68}/tests/test_http_client_compat.py +0 -0
  106. {axis-67 → axis-68}/tests/test_pir_sensor_configuration.py +0 -0
  107. {axis-67 → axis-68}/tests/test_port_cgi.py +0 -0
  108. {axis-67 → axis-68}/tests/test_port_management.py +0 -0
  109. {axis-67 → axis-68}/tests/test_ptz.py +0 -0
  110. {axis-67 → axis-68}/tests/test_pwdgrp_cgi.py +0 -0
  111. {axis-67 → axis-68}/tests/test_stream_profiles.py +0 -0
  112. {axis-67 → axis-68}/tests/test_user_groups.py +0 -0
  113. {axis-67 → axis-68}/tests/test_view_areas.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: axis
3
- Version: 67
3
+ Version: 68
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
@@ -24,22 +24,75 @@ Requires-Dist: orjson>3.9
24
24
  Requires-Dist: packaging>23
25
25
  Requires-Dist: xmltodict>=0.13.0
26
26
  Provides-Extra: requirements
27
- Requires-Dist: aiohttp==3.13.1; extra == "requirements"
27
+ Requires-Dist: aiohttp==3.13.5; extra == "requirements"
28
28
  Requires-Dist: httpx==0.28.1; extra == "requirements"
29
- Requires-Dist: orjson==3.11.7; extra == "requirements"
29
+ Requires-Dist: orjson==3.11.8; extra == "requirements"
30
30
  Requires-Dist: packaging==26.0; extra == "requirements"
31
31
  Requires-Dist: xmltodict==1.0.4; extra == "requirements"
32
32
  Provides-Extra: requirements-test
33
- Requires-Dist: mypy==1.19.1; extra == "requirements-test"
33
+ Requires-Dist: mypy==1.20.0; extra == "requirements-test"
34
34
  Requires-Dist: pytest==9.0.2; extra == "requirements-test"
35
35
  Requires-Dist: pytest-aiohttp==1.1.0; extra == "requirements-test"
36
36
  Requires-Dist: pytest-asyncio==1.3.0; extra == "requirements-test"
37
- Requires-Dist: pytest-cov==7.0.0; extra == "requirements-test"
37
+ Requires-Dist: pytest-cov==7.1.0; extra == "requirements-test"
38
38
  Requires-Dist: respx==0.22.0; extra == "requirements-test"
39
- Requires-Dist: ruff==0.15.6; extra == "requirements-test"
39
+ Requires-Dist: ruff==0.15.9; extra == "requirements-test"
40
40
  Requires-Dist: types-xmltodict==v1.0.1.20260113; extra == "requirements-test"
41
41
  Provides-Extra: requirements-dev
42
42
  Requires-Dist: pre-commit==4.5.1; extra == "requirements-dev"
43
43
  Dynamic: license-file
44
44
 
45
+ # axis
46
+
45
47
  Python project to set up a connection towards Axis Communications devices and to subscribe to specific events on the metadatastream.
48
+
49
+ ## Development setup
50
+
51
+ `uv` is required for development setup:
52
+
53
+ ```bash
54
+ uv python install 3.14
55
+ uv sync --python 3.14 --all-extras
56
+ ```
57
+
58
+ Or run the bootstrap script, which installs `uv` if needed and provisions Python 3.14 automatically:
59
+
60
+ ```bash
61
+ ./setup.sh
62
+ ```
63
+
64
+ Dependencies are locked via `uv.lock`. Regenerate lock data when dependency inputs change:
65
+
66
+ ```bash
67
+ uv lock
68
+ ```
69
+
70
+ Run checks with `uv`:
71
+
72
+ ```bash
73
+ uv run ruff check .
74
+ uv run ruff format --check .
75
+ uv run mypy axis
76
+ uv run pytest
77
+ ```
78
+
79
+ Initial `ty` support is configured as an opt-in check and does not replace `mypy`:
80
+
81
+ ```bash
82
+ uvx ty check
83
+ ```
84
+
85
+ ## Initialization architecture
86
+
87
+ Vapix initialization is phase-based and driven by handler metadata:
88
+
89
+ - `API_DISCOVERY`: handlers initialized after API discovery.
90
+ - `PARAM_CGI_FALLBACK`: handlers that may initialize from parameter support when not listed in discovery.
91
+ - `APPLICATION`: handlers initialized after applications are loaded.
92
+
93
+ Handlers declare phase membership through `handler_groups` and may customize phase eligibility through `should_initialize_in_group`.
94
+
95
+ Example fallback policy:
96
+
97
+ - `LightHandler` participates in both `API_DISCOVERY` and `PARAM_CGI_FALLBACK`.
98
+ - In `PARAM_CGI_FALLBACK`, it initializes only when not listed in API discovery and listed in parameters.
axis-68/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # axis
2
+
3
+ Python project to set up a connection towards Axis Communications devices and to subscribe to specific events on the metadatastream.
4
+
5
+ ## Development setup
6
+
7
+ `uv` is required for development setup:
8
+
9
+ ```bash
10
+ uv python install 3.14
11
+ uv sync --python 3.14 --all-extras
12
+ ```
13
+
14
+ Or run the bootstrap script, which installs `uv` if needed and provisions Python 3.14 automatically:
15
+
16
+ ```bash
17
+ ./setup.sh
18
+ ```
19
+
20
+ Dependencies are locked via `uv.lock`. Regenerate lock data when dependency inputs change:
21
+
22
+ ```bash
23
+ uv lock
24
+ ```
25
+
26
+ Run checks with `uv`:
27
+
28
+ ```bash
29
+ uv run ruff check .
30
+ uv run ruff format --check .
31
+ uv run mypy axis
32
+ uv run pytest
33
+ ```
34
+
35
+ Initial `ty` support is configured as an opt-in check and does not replace `mypy`:
36
+
37
+ ```bash
38
+ uvx ty check
39
+ ```
40
+
41
+ ## Initialization architecture
42
+
43
+ Vapix initialization is phase-based and driven by handler metadata:
44
+
45
+ - `API_DISCOVERY`: handlers initialized after API discovery.
46
+ - `PARAM_CGI_FALLBACK`: handlers that may initialize from parameter support when not listed in discovery.
47
+ - `APPLICATION`: handlers initialized after applications are loaded.
48
+
49
+ Handlers declare phase membership through `handler_groups` and may customize phase eligibility through `should_initialize_in_group`.
50
+
51
+ Example fallback policy:
52
+
53
+ - `LightHandler` participates in both `API_DISCOVERY` and `PARAM_CGI_FALLBACK`.
54
+ - In `PARAM_CGI_FALLBACK`, it initializes only when not listed in API discovery and listed in parameters.
@@ -28,10 +28,12 @@ async def axis_device(
28
28
  username: str,
29
29
  password: str,
30
30
  web_proto: WebProtocol,
31
+ stream_mode: str = "rtsp",
31
32
  is_companion: bool = False,
32
33
  ) -> axis.device.AxisDevice:
33
34
  """Create a Axis device."""
34
35
  session = create_session()
36
+ websocket_enabled, websocket_force = websocket_flags_from_mode(stream_mode)
35
37
  device = AxisDevice(
36
38
  Configuration(
37
39
  session,
@@ -41,6 +43,8 @@ async def axis_device(
41
43
  password=password,
42
44
  is_companion=is_companion,
43
45
  web_proto=web_proto,
46
+ websocket_enabled=websocket_enabled,
47
+ websocket_force=websocket_force,
44
48
  )
45
49
  )
46
50
 
@@ -74,6 +78,7 @@ async def main(
74
78
  params: bool,
75
79
  events: bool,
76
80
  web_proto: WebProtocol,
81
+ stream_mode: str,
77
82
  ) -> None:
78
83
  """CLI method for library."""
79
84
  LOGGER.info("Connecting to Axis device")
@@ -84,6 +89,7 @@ async def main(
84
89
  username,
85
90
  password,
86
91
  web_proto=web_proto,
92
+ stream_mode=stream_mode,
87
93
  )
88
94
 
89
95
  if not device:
@@ -126,6 +132,15 @@ async def close_session(session: aiohttp.ClientSession) -> None:
126
132
  await session.close()
127
133
 
128
134
 
135
+ def websocket_flags_from_mode(stream_mode: str) -> tuple[bool, bool]:
136
+ """Translate CLI stream mode to websocket enable/force flags."""
137
+ if stream_mode == "auto":
138
+ return True, False
139
+ if stream_mode == "event":
140
+ return True, True
141
+ return False, False
142
+
143
+
129
144
  if __name__ == "__main__":
130
145
  parser = argparse.ArgumentParser()
131
146
  parser.add_argument("host", type=str)
@@ -135,6 +150,11 @@ if __name__ == "__main__":
135
150
  parser.add_argument("--proto", type=str, default="http")
136
151
  parser.add_argument("--events", action="store_true")
137
152
  parser.add_argument("--params", action="store_true")
153
+ parser.add_argument(
154
+ "--stream-mode",
155
+ choices=["auto", "rtsp", "event"],
156
+ default="rtsp",
157
+ )
138
158
  parser.add_argument("-D", "--debug", action="store_true")
139
159
  args = parser.parse_args()
140
160
 
@@ -163,6 +183,7 @@ if __name__ == "__main__":
163
183
  params=args.params,
164
184
  events=args.events,
165
185
  web_proto=WebProtocol(args.proto),
186
+ stream_mode=args.stream_mode,
166
187
  )
167
188
  )
168
189
 
@@ -8,6 +8,7 @@ from collections.abc import (
8
8
  Sequence,
9
9
  ValuesView,
10
10
  )
11
+ import enum
11
12
  from typing import TYPE_CHECKING, Generic, final
12
13
 
13
14
  from ..errors import Forbidden, PathNotFound, Unauthorized
@@ -25,6 +26,14 @@ UnsubscribeType = Callable[[], None]
25
26
  ID_FILTER_ALL = "*"
26
27
 
27
28
 
29
+ class HandlerGroup(enum.Enum):
30
+ """Group handlers by initialization phase in Vapix."""
31
+
32
+ API_DISCOVERY = "api_discovery"
33
+ PARAM_CGI_FALLBACK = "param_cgi_fallback"
34
+ APPLICATION = "application"
35
+
36
+
28
37
  class SubscriptionHandler:
29
38
  """Manage subscription and notification to subscribers."""
30
39
 
@@ -75,12 +84,14 @@ class ApiHandler(SubscriptionHandler, Generic[ApiItemT]):
75
84
 
76
85
  api_id: ApiId | None = None
77
86
  default_api_version: str | None = None
87
+ handler_groups: tuple[HandlerGroup, ...] = ()
78
88
  skip_support_check = False
79
89
 
80
90
  def __init__(self, vapix: Vapix) -> None:
81
91
  """Initialize API items."""
82
92
  super().__init__()
83
93
  self.vapix = vapix
94
+ self.vapix._register_handler(self)
84
95
  self._items: dict[str, ApiItemT] = {}
85
96
  self.initialized = False
86
97
 
@@ -101,6 +112,10 @@ class ApiHandler(SubscriptionHandler, Generic[ApiItemT]):
101
112
  """Is API listed in parameters."""
102
113
  return False
103
114
 
115
+ def should_initialize_in_group(self, group: HandlerGroup) -> bool:
116
+ """Return whether handler should initialize in the given group."""
117
+ return True
118
+
104
119
  async def _api_request(self) -> dict[str, ApiItemT]:
105
120
  """Get API data method defined by subclass."""
106
121
  raise NotImplementedError
@@ -134,15 +149,41 @@ class ApiHandler(SubscriptionHandler, Generic[ApiItemT]):
134
149
  return self.initialized
135
150
 
136
151
  @property
137
- def api_version(self) -> str | None:
138
- """Latest API version supported."""
139
- if (
140
- self.api_id is not None
141
- and (discovery_item := self.vapix.api_discovery.get(self.api_id))
142
- is not None
143
- ):
144
- return discovery_item.version
145
- return self.default_api_version
152
+ def api_version(self) -> str:
153
+ """Latest API version supported.
154
+
155
+ Returns the API version in this order of precedence:
156
+ 1. Version from device discovery (dynamic, device-specific)
157
+ 2. Handler's default_api_version (static, library-defined)
158
+ 3. Empty string "" (for handlers without discovery support)
159
+
160
+ Note: This property always returns a string (never None). Handlers that don't
161
+ support API versioning (no api_id) return empty string, which is safe because
162
+ they don't send api_version in their requests.
163
+
164
+ Returns:
165
+ str: API version (e.g., "1.0", "1.1") or empty string if not available.
166
+
167
+ """
168
+ discovery_version = self._get_api_discovery_version()
169
+ if discovery_version is not None:
170
+ return discovery_version
171
+ return self.default_api_version or ""
172
+
173
+ def _get_api_discovery_version(self) -> str | None:
174
+ """Get API version from discovery data when available."""
175
+ if self.api_id is None:
176
+ return None
177
+
178
+ discovery_item = self.vapix.api_discovery.get(self.api_id)
179
+ if discovery_item is None:
180
+ return None
181
+
182
+ discovery_version = getattr(discovery_item, "version", None)
183
+ if isinstance(discovery_version, str):
184
+ return discovery_version
185
+
186
+ return None
146
187
 
147
188
  def items(self) -> ItemsView[str, ApiItemT]:
148
189
  """Return items."""
@@ -4,13 +4,14 @@ from abc import abstractmethod
4
4
 
5
5
  from ...models.api import ApiItemT
6
6
  from ...models.applications.application import ApplicationName, ApplicationStatus
7
- from ..api_handler import ApiHandler
7
+ from ..api_handler import ApiHandler, HandlerGroup
8
8
 
9
9
 
10
10
  class ApplicationHandler(ApiHandler[ApiItemT]):
11
11
  """Generic application handler."""
12
12
 
13
13
  app_name: ApplicationName
14
+ handler_groups = (HandlerGroup.APPLICATION,)
14
15
 
15
16
  @property
16
17
  def supported(self) -> bool:
@@ -14,7 +14,7 @@ from ..models.basic_device_info import (
14
14
  GetSupportedVersionsRequest,
15
15
  GetSupportedVersionsResponse,
16
16
  )
17
- from .api_handler import ApiHandler
17
+ from .api_handler import ApiHandler, HandlerGroup
18
18
 
19
19
 
20
20
  class BasicDeviceInfoHandler(ApiHandler[DeviceInformation]):
@@ -22,6 +22,7 @@ class BasicDeviceInfoHandler(ApiHandler[DeviceInformation]):
22
22
 
23
23
  api_id = ApiId.BASIC_DEVICE_INFO
24
24
  default_api_version = API_VERSION
25
+ handler_groups = (HandlerGroup.API_DISCOVERY,)
25
26
 
26
27
  async def _api_request(self) -> dict[str, DeviceInformation]:
27
28
  """Get default data of basic device information."""
@@ -29,9 +30,8 @@ class BasicDeviceInfoHandler(ApiHandler[DeviceInformation]):
29
30
 
30
31
  async def get_all_properties(self) -> dict[str, DeviceInformation]:
31
32
  """List all properties of basic device info."""
32
- discovery_item = self.vapix.api_discovery[self.api_id]
33
33
  bytes_data = await self.vapix.api_request(
34
- GetAllPropertiesRequest(discovery_item.version)
34
+ GetAllPropertiesRequest(self.api_version)
35
35
  )
36
36
  response = GetAllPropertiesResponse.decode(bytes_data)
37
37
  return {"0": response.data}
@@ -45,7 +45,7 @@ from ..models.light_control import (
45
45
  SetManualAngleOfIlluminationModeRequest,
46
46
  SetManualIntensityRequest,
47
47
  )
48
- from .api_handler import ApiHandler
48
+ from .api_handler import ApiHandler, HandlerGroup
49
49
 
50
50
 
51
51
  class LightHandler(ApiHandler[LightInformation]):
@@ -53,6 +53,10 @@ class LightHandler(ApiHandler[LightInformation]):
53
53
 
54
54
  api_id = ApiId.LIGHT_CONTROL
55
55
  default_api_version = API_VERSION
56
+ handler_groups = (
57
+ HandlerGroup.API_DISCOVERY,
58
+ HandlerGroup.PARAM_CGI_FALLBACK,
59
+ )
56
60
 
57
61
  @property
58
62
  def listed_in_parameters(self) -> bool:
@@ -61,6 +65,12 @@ class LightHandler(ApiHandler[LightInformation]):
61
65
  return prop.light_control
62
66
  return False
63
67
 
68
+ def should_initialize_in_group(self, group: HandlerGroup) -> bool:
69
+ """Return whether handler should initialize in the given group."""
70
+ if group is HandlerGroup.PARAM_CGI_FALLBACK:
71
+ return not self.listed_in_api_discovery and self.listed_in_parameters
72
+ return True
73
+
64
74
  async def _api_request(self) -> dict[str, LightInformation]:
65
75
  """Get default data of stream profiles."""
66
76
  return await self.get_light_information()
@@ -68,51 +78,45 @@ class LightHandler(ApiHandler[LightInformation]):
68
78
  async def get_light_information(self) -> dict[str, LightInformation]:
69
79
  """List the light control information."""
70
80
  bytes_data = await self.vapix.api_request(
71
- GetLightInformationRequest(api_version=self.default_api_version)
81
+ GetLightInformationRequest(api_version=self.api_version)
72
82
  )
73
83
  return GetLightInformationResponse.decode(bytes_data).data
74
84
 
75
85
  async def get_service_capabilities(self) -> ServiceCapabilities:
76
86
  """List the light control information."""
77
87
  bytes_data = await self.vapix.api_request(
78
- GetServiceCapabilitiesRequest(api_version=self.default_api_version)
88
+ GetServiceCapabilitiesRequest(api_version=self.api_version)
79
89
  )
80
90
  return GetServiceCapabilitiesResponse.decode(bytes_data).data
81
91
 
82
92
  async def activate_light(self, light_id: str) -> None:
83
93
  """Activate the light."""
84
94
  await self.vapix.api_request(
85
- ActivateLightRequest(
86
- api_version=self.default_api_version, light_id=light_id
87
- )
95
+ ActivateLightRequest(api_version=self.api_version, light_id=light_id)
88
96
  )
89
97
 
90
98
  async def deactivate_light(self, light_id: str) -> None:
91
99
  """Deactivate the light."""
92
100
  await self.vapix.api_request(
93
- DeactivateLightRequest(
94
- api_version=self.default_api_version, light_id=light_id
95
- )
101
+ DeactivateLightRequest(api_version=self.api_version, light_id=light_id)
96
102
  )
97
103
 
98
104
  async def enable_light(self, light_id: str) -> None:
99
105
  """Activate the light."""
100
106
  await self.vapix.api_request(
101
- EnableLightRequest(api_version=self.default_api_version, light_id=light_id)
107
+ EnableLightRequest(api_version=self.api_version, light_id=light_id)
102
108
  )
103
109
 
104
110
  async def disable_light(self, light_id: str) -> None:
105
111
  """Deactivate the light."""
106
112
  await self.vapix.api_request(
107
- DisableLightRequest(api_version=self.default_api_version, light_id=light_id)
113
+ DisableLightRequest(api_version=self.api_version, light_id=light_id)
108
114
  )
109
115
 
110
116
  async def get_light_status(self, light_id: str) -> bool:
111
117
  """Get light status if its on or off."""
112
118
  bytes_data = await self.vapix.api_request(
113
- GetLightStatusRequest(
114
- api_version=self.default_api_version, light_id=light_id
115
- )
119
+ GetLightStatusRequest(api_version=self.api_version, light_id=light_id)
116
120
  )
117
121
  return GetLightStatusResponse.decode(bytes_data).data
118
122
 
@@ -120,7 +124,7 @@ class LightHandler(ApiHandler[LightInformation]):
120
124
  """Enable the automatic light intensity control."""
121
125
  await self.vapix.api_request(
122
126
  SetAutomaticIntensityModeRequest(
123
- api_version=self.default_api_version,
127
+ api_version=self.api_version,
124
128
  light_id=light_id,
125
129
  enabled=enabled,
126
130
  )
@@ -129,9 +133,7 @@ class LightHandler(ApiHandler[LightInformation]):
129
133
  async def get_valid_intensity(self, light_id: str) -> Range:
130
134
  """Get valid intensity range for light."""
131
135
  bytes_data = await self.vapix.api_request(
132
- GetValidIntensityRequest(
133
- api_version=self.default_api_version, light_id=light_id
134
- )
136
+ GetValidIntensityRequest(api_version=self.api_version, light_id=light_id)
135
137
  )
136
138
  return GetValidIntensityResponse.decode(bytes_data).data
137
139
 
@@ -139,7 +141,7 @@ class LightHandler(ApiHandler[LightInformation]):
139
141
  """Manually sets the intensity."""
140
142
  await self.vapix.api_request(
141
143
  SetManualIntensityRequest(
142
- api_version=self.default_api_version,
144
+ api_version=self.api_version,
143
145
  light_id=light_id,
144
146
  intensity=intensity,
145
147
  )
@@ -148,9 +150,7 @@ class LightHandler(ApiHandler[LightInformation]):
148
150
  async def get_manual_intensity(self, light_id: str) -> int:
149
151
  """Enable the automatic light intensity control."""
150
152
  bytes_data = await self.vapix.api_request(
151
- GetManualIntensityRequest(
152
- api_version=self.default_api_version, light_id=light_id
153
- )
153
+ GetManualIntensityRequest(api_version=self.api_version, light_id=light_id)
154
154
  )
155
155
  return GetManualIntensityResponse.decode(bytes_data).data
156
156
 
@@ -160,7 +160,7 @@ class LightHandler(ApiHandler[LightInformation]):
160
160
  """Manually sets the intensity for an individual LED."""
161
161
  await self.vapix.api_request(
162
162
  SetIndividualIntensityRequest(
163
- api_version=self.default_api_version,
163
+ api_version=self.api_version,
164
164
  light_id=light_id,
165
165
  led_id=led_id,
166
166
  intensity=intensity,
@@ -171,7 +171,7 @@ class LightHandler(ApiHandler[LightInformation]):
171
171
  """Receives the intensity from the setIndividualIntensity request."""
172
172
  bytes_data = await self.vapix.api_request(
173
173
  GetIndividualIntensityRequest(
174
- api_version=self.default_api_version,
174
+ api_version=self.api_version,
175
175
  light_id=light_id,
176
176
  led_id=led_id,
177
177
  )
@@ -181,9 +181,7 @@ class LightHandler(ApiHandler[LightInformation]):
181
181
  async def get_current_intensity(self, light_id: str) -> int:
182
182
  """Receives the intensity from the setIndividualIntensity request."""
183
183
  bytes_data = await self.vapix.api_request(
184
- GetCurrentIntensityRequest(
185
- api_version=self.default_api_version, light_id=light_id
186
- )
184
+ GetCurrentIntensityRequest(api_version=self.api_version, light_id=light_id)
187
185
  )
188
186
  return GetCurrentIntensityResponse.decode(bytes_data).data
189
187
 
@@ -197,7 +195,9 @@ class LightHandler(ApiHandler[LightInformation]):
197
195
  """
198
196
  await self.vapix.api_request(
199
197
  SetAutomaticAngleOfIlluminationModeRequest(
200
- api_version=self.default_api_version, light_id=light_id, enabled=enabled
198
+ api_version=self.api_version,
199
+ light_id=light_id,
200
+ enabled=enabled,
201
201
  )
202
202
  )
203
203
 
@@ -205,7 +205,7 @@ class LightHandler(ApiHandler[LightInformation]):
205
205
  """List the valid angle of illumination values."""
206
206
  bytes_data = await self.vapix.api_request(
207
207
  GetValidAngleOfIlluminationRequest(
208
- api_version=self.default_api_version, light_id=light_id
208
+ api_version=self.api_version, light_id=light_id
209
209
  )
210
210
  )
211
211
  return GetValidAngleOfIlluminationResponse.decode(bytes_data).data
@@ -220,7 +220,7 @@ class LightHandler(ApiHandler[LightInformation]):
220
220
  """
221
221
  await self.vapix.api_request(
222
222
  SetManualAngleOfIlluminationModeRequest(
223
- api_version=self.default_api_version,
223
+ api_version=self.api_version,
224
224
  light_id=light_id,
225
225
  angle_of_illumination=angle_of_illumination,
226
226
  )
@@ -230,7 +230,7 @@ class LightHandler(ApiHandler[LightInformation]):
230
230
  """Get the angle of illumination."""
231
231
  bytes_data = await self.vapix.api_request(
232
232
  GetManualAngleOfIlluminationRequest(
233
- api_version=self.default_api_version, light_id=light_id
233
+ api_version=self.api_version, light_id=light_id
234
234
  )
235
235
  )
236
236
  return GetManualAngleOfIlluminationResponse.decode(bytes_data).data
@@ -239,7 +239,7 @@ class LightHandler(ApiHandler[LightInformation]):
239
239
  """Receive the current angle of illumination."""
240
240
  bytes_data = await self.vapix.api_request(
241
241
  GetCurrentAngleOfIlluminationRequest(
242
- api_version=self.default_api_version, light_id=light_id
242
+ api_version=self.api_version, light_id=light_id
243
243
  )
244
244
  )
245
245
  return GetCurrentAngleOfIlluminationResponse.decode(bytes_data).data
@@ -250,7 +250,9 @@ class LightHandler(ApiHandler[LightInformation]):
250
250
  """Enable automatic synchronization with the day/night mode."""
251
251
  await self.vapix.api_request(
252
252
  SetLightSynchronizeDayNightModeRequest(
253
- api_version=self.default_api_version, light_id=light_id, enabled=enabled
253
+ api_version=self.api_version,
254
+ light_id=light_id,
255
+ enabled=enabled,
254
256
  )
255
257
  )
256
258
 
@@ -258,7 +260,7 @@ class LightHandler(ApiHandler[LightInformation]):
258
260
  """Check if the automatic synchronization is enabled with the day/night mode."""
259
261
  bytes_data = await self.vapix.api_request(
260
262
  GetLightSynchronizeDayNightModeRequest(
261
- api_version=self.default_api_version, light_id=light_id
263
+ api_version=self.api_version, light_id=light_id
262
264
  )
263
265
  )
264
266
  return GetLightSynchronizeDayNightModeResponse.decode(bytes_data).data
@@ -20,23 +20,28 @@ from ..models.mqtt import (
20
20
  GetEventPublicationConfigRequest,
21
21
  GetEventPublicationConfigResponse,
22
22
  )
23
- from .api_handler import ApiHandler
23
+ from .api_handler import ApiHandler, HandlerGroup
24
24
 
25
25
  DEFAULT_TOPICS = ["//."]
26
26
 
27
27
 
28
28
  def mqtt_json_to_event(msg: bytes | bytearray | memoryview | str) -> dict[str, Any]:
29
- """Convert JSON message from MQTT to event format."""
29
+ """Convert JSON message from MQTT to event format.
30
+
31
+ Returns None if required keys are missing.
32
+ """
30
33
  message = orjson.loads(msg)
31
- topic = message["topic"].replace("onvif", "tns1").replace("axis", "tnsaxis")
34
+ topic = source = source_idx = data_type = data_value = ""
32
35
 
33
- source = source_idx = ""
34
- if message["message"]["source"]:
35
- source, source_idx = next(iter(message["message"]["source"].items()))
36
+ if "topic" in message:
37
+ topic = message["topic"].replace("onvif", "tns1").replace("axis", "tnsaxis")
36
38
 
37
- data_type = data_value = ""
38
- if message["message"]["data"]:
39
- data_type, data_value = next(iter(message["message"]["data"].items()))
39
+ msg_message = message.get("message", {})
40
+ if isinstance(msg_message, dict):
41
+ if source_dict := msg_message.get("source"):
42
+ source, source_idx = next(iter(source_dict.items()))
43
+ if data_dict := msg_message.get("data"):
44
+ data_type, data_value = next(iter(data_dict.items()))
40
45
 
41
46
  return {
42
47
  "topic": topic,
@@ -52,31 +57,32 @@ class MqttClientHandler(ApiHandler[Any]):
52
57
 
53
58
  api_id = ApiId.MQTT_CLIENT
54
59
  default_api_version = API_VERSION
60
+ handler_groups = (HandlerGroup.API_DISCOVERY,)
55
61
 
56
62
  async def configure_client(self, client_config: ClientConfig) -> None:
57
63
  """Configure MQTT Client."""
58
64
  await self.vapix.api_request(
59
65
  ConfigureClientRequest(
60
- api_version=self.default_api_version, client_config=client_config
66
+ api_version=self.api_version, client_config=client_config
61
67
  )
62
68
  )
63
69
 
64
70
  async def activate(self) -> None:
65
71
  """Activate MQTT Client."""
66
72
  await self.vapix.api_request(
67
- ActivateClientRequest(api_version=self.default_api_version)
73
+ ActivateClientRequest(api_version=self.api_version)
68
74
  )
69
75
 
70
76
  async def deactivate(self) -> None:
71
77
  """Deactivate MQTT Client."""
72
78
  await self.vapix.api_request(
73
- DeactivateClientRequest(api_version=self.default_api_version)
79
+ DeactivateClientRequest(api_version=self.api_version)
74
80
  )
75
81
 
76
82
  async def get_client_status(self) -> ClientConfigStatus:
77
83
  """Get MQTT Client status."""
78
84
  bytes_data = await self.vapix.api_request(
79
- GetClientStatusRequest(api_version=self.default_api_version)
85
+ GetClientStatusRequest(api_version=self.api_version)
80
86
  )
81
87
  response = GetClientStatusResponse.decode(bytes_data)
82
88
  return response.data
@@ -84,7 +90,7 @@ class MqttClientHandler(ApiHandler[Any]):
84
90
  async def get_event_publication_config(self) -> EventPublicationConfig:
85
91
  """Get MQTT Client event publication config."""
86
92
  bytes_data = await self.vapix.api_request(
87
- GetEventPublicationConfigRequest(api_version=self.default_api_version)
93
+ GetEventPublicationConfigRequest(api_version=self.api_version)
88
94
  )
89
95
  response = GetEventPublicationConfigResponse.decode(bytes_data)
90
96
  return response.data
@@ -99,6 +105,6 @@ class MqttClientHandler(ApiHandler[Any]):
99
105
  config = EventPublicationConfig(event_filter_list=event_filters)
100
106
  await self.vapix.api_request(
101
107
  ConfigureEventPublicationRequest(
102
- api_version=self.default_api_version, config=config
108
+ api_version=self.api_version, config=config
103
109
  )
104
110
  )