axis 58__tar.gz → 60__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 (111) hide show
  1. axis-60/PKG-INFO +24 -0
  2. {axis-58 → axis-60}/axis/__main__.py +1 -1
  3. {axis-58 → axis-60}/axis/device.py +4 -4
  4. {axis-58/axis/vapix → axis-60/axis}/interfaces/api_handler.py +2 -2
  5. axis-58/axis/event_stream.py → axis-60/axis/interfaces/event_manager.py +3 -8
  6. {axis-58/axis/vapix → axis-60/axis}/interfaces/mqtt.py +7 -14
  7. {axis-58/axis/vapix → axis-60/axis}/interfaces/parameters/param_cgi.py +1 -1
  8. {axis-58/axis/vapix → axis-60/axis/interfaces}/vapix.py +27 -25
  9. {axis-58/axis/vapix → axis-60/axis}/models/event_instance.py +1 -1
  10. {axis-58/axis/vapix → axis-60/axis}/models/light_control.py +9 -7
  11. {axis-58/axis/vapix → axis-60/axis}/models/mqtt.py +178 -19
  12. {axis-58/axis/vapix → axis-60/axis}/models/parameters/properties.py +3 -2
  13. axis-60/axis.egg-info/PKG-INFO +24 -0
  14. axis-60/axis.egg-info/SOURCES.txt +103 -0
  15. {axis-58 → axis-60}/axis.egg-info/requires.txt +4 -4
  16. {axis-58 → axis-60}/pyproject.toml +5 -5
  17. {axis-58 → axis-60}/tests/test_api_discovery.py +2 -2
  18. {axis-58 → axis-60}/tests/test_api_handler.py +1 -1
  19. {axis-58 → axis-60}/tests/test_basic_device_info.py +1 -1
  20. {axis-58 → axis-60}/tests/test_configuration.py +1 -1
  21. {axis-58 → axis-60}/tests/test_event_instances.py +2 -2
  22. {axis-58 → axis-60}/tests/test_event_stream.py +1 -1
  23. {axis-58 → axis-60}/tests/test_light_control.py +20 -2
  24. {axis-58 → axis-60}/tests/test_mqtt.py +6 -3
  25. {axis-58 → axis-60}/tests/test_pir_sensor_configuration.py +2 -2
  26. {axis-58 → axis-60}/tests/test_port_cgi.py +2 -2
  27. {axis-58 → axis-60}/tests/test_port_management.py +2 -2
  28. {axis-58 → axis-60}/tests/test_ptz.py +2 -2
  29. {axis-58 → axis-60}/tests/test_pwdgrp_cgi.py +2 -2
  30. {axis-58 → axis-60}/tests/test_stream_profiles.py +1 -1
  31. {axis-58 → axis-60}/tests/test_user_groups.py +2 -2
  32. {axis-58 → axis-60}/tests/test_vapix.py +4 -4
  33. {axis-58 → axis-60}/tests/test_view_areas.py +1 -1
  34. axis-58/PKG-INFO +0 -45
  35. axis-58/axis/vapix/__init__.py +0 -1
  36. axis-58/axis/vapix/models/__init__.py +0 -1
  37. axis-58/axis.egg-info/PKG-INFO +0 -45
  38. axis-58/axis.egg-info/SOURCES.txt +0 -106
  39. axis-58/setup.py +0 -25
  40. {axis-58 → axis-60}/LICENSE +0 -0
  41. {axis-58 → axis-60}/README.md +0 -0
  42. {axis-58 → axis-60}/axis/__init__.py +0 -0
  43. {axis-58 → axis-60}/axis/errors.py +0 -0
  44. {axis-58/axis/vapix → axis-60/axis}/interfaces/__init__.py +0 -0
  45. {axis-58/axis/vapix → axis-60/axis}/interfaces/api_discovery.py +0 -0
  46. {axis-58/axis/vapix → axis-60/axis}/interfaces/applications/__init__.py +0 -0
  47. {axis-58/axis/vapix → axis-60/axis}/interfaces/applications/application_handler.py +0 -0
  48. {axis-58/axis/vapix → axis-60/axis}/interfaces/applications/applications.py +0 -0
  49. {axis-58/axis/vapix → axis-60/axis}/interfaces/applications/fence_guard.py +0 -0
  50. {axis-58/axis/vapix → axis-60/axis}/interfaces/applications/loitering_guard.py +0 -0
  51. {axis-58/axis/vapix → axis-60/axis}/interfaces/applications/motion_guard.py +0 -0
  52. {axis-58/axis/vapix → axis-60/axis}/interfaces/applications/object_analytics.py +0 -0
  53. {axis-58/axis/vapix → axis-60/axis}/interfaces/applications/vmd4.py +0 -0
  54. {axis-58/axis/vapix → axis-60/axis}/interfaces/basic_device_info.py +0 -0
  55. {axis-58/axis/vapix → axis-60/axis}/interfaces/event_instances.py +0 -0
  56. {axis-58/axis/vapix → axis-60/axis}/interfaces/light_control.py +0 -0
  57. {axis-58/axis/vapix → axis-60/axis}/interfaces/parameters/__init__.py +0 -0
  58. {axis-58/axis/vapix → axis-60/axis}/interfaces/parameters/brand.py +0 -0
  59. {axis-58/axis/vapix → axis-60/axis}/interfaces/parameters/image.py +0 -0
  60. {axis-58/axis/vapix → axis-60/axis}/interfaces/parameters/io_port.py +0 -0
  61. {axis-58/axis/vapix → axis-60/axis}/interfaces/parameters/param_handler.py +0 -0
  62. {axis-58/axis/vapix → axis-60/axis}/interfaces/parameters/properties.py +0 -0
  63. {axis-58/axis/vapix → axis-60/axis}/interfaces/parameters/ptz.py +0 -0
  64. {axis-58/axis/vapix → axis-60/axis}/interfaces/parameters/stream_profile.py +0 -0
  65. {axis-58/axis/vapix → axis-60/axis}/interfaces/pir_sensor_configuration.py +0 -0
  66. {axis-58/axis/vapix → axis-60/axis}/interfaces/port_cgi.py +0 -0
  67. {axis-58/axis/vapix → axis-60/axis}/interfaces/port_management.py +0 -0
  68. {axis-58/axis/vapix → axis-60/axis}/interfaces/ptz.py +0 -0
  69. {axis-58/axis/vapix → axis-60/axis}/interfaces/pwdgrp_cgi.py +0 -0
  70. {axis-58/axis/vapix → axis-60/axis}/interfaces/stream_profiles.py +0 -0
  71. {axis-58/axis/vapix → axis-60/axis}/interfaces/user_groups.py +0 -0
  72. {axis-58/axis/vapix → axis-60/axis}/interfaces/view_areas.py +0 -0
  73. {axis-58 → axis-60}/axis/models/__init__.py +0 -0
  74. {axis-58/axis/vapix → axis-60/axis}/models/api.py +0 -0
  75. {axis-58/axis/vapix → axis-60/axis}/models/api_discovery.py +0 -0
  76. {axis-58/axis/vapix → axis-60/axis}/models/applications/__init__.py +0 -0
  77. {axis-58/axis/vapix → axis-60/axis}/models/applications/application.py +0 -0
  78. {axis-58/axis/vapix → axis-60/axis}/models/applications/fence_guard.py +0 -0
  79. {axis-58/axis/vapix → axis-60/axis}/models/applications/loitering_guard.py +0 -0
  80. {axis-58/axis/vapix → axis-60/axis}/models/applications/motion_guard.py +0 -0
  81. {axis-58/axis/vapix → axis-60/axis}/models/applications/object_analytics.py +0 -0
  82. {axis-58/axis/vapix → axis-60/axis}/models/applications/vmd4.py +0 -0
  83. {axis-58/axis/vapix → axis-60/axis}/models/basic_device_info.py +0 -0
  84. {axis-58/axis → axis-60/axis/models}/configuration.py +0 -0
  85. {axis-58 → axis-60}/axis/models/event.py +0 -0
  86. {axis-58/axis/vapix → axis-60/axis}/models/parameters/__init__.py +0 -0
  87. {axis-58/axis/vapix → axis-60/axis}/models/parameters/brand.py +0 -0
  88. {axis-58/axis/vapix → axis-60/axis}/models/parameters/image.py +0 -0
  89. {axis-58/axis/vapix → axis-60/axis}/models/parameters/io_port.py +0 -0
  90. {axis-58/axis/vapix → axis-60/axis}/models/parameters/param_cgi.py +0 -0
  91. {axis-58/axis/vapix → axis-60/axis}/models/parameters/ptz.py +0 -0
  92. {axis-58/axis/vapix → axis-60/axis}/models/parameters/stream_profile.py +0 -0
  93. {axis-58/axis/vapix → axis-60/axis}/models/pir_sensor_configuration.py +0 -0
  94. {axis-58/axis/vapix → axis-60/axis}/models/port_cgi.py +0 -0
  95. {axis-58/axis/vapix → axis-60/axis}/models/port_management.py +0 -0
  96. {axis-58/axis/vapix → axis-60/axis}/models/ptz_cgi.py +0 -0
  97. {axis-58/axis/vapix → axis-60/axis}/models/pwdgrp_cgi.py +0 -0
  98. {axis-58/axis/vapix → axis-60/axis}/models/stream_profile.py +0 -0
  99. {axis-58/axis/vapix → axis-60/axis}/models/user_group.py +0 -0
  100. {axis-58/axis/vapix → axis-60/axis}/models/view_area.py +0 -0
  101. {axis-58 → axis-60}/axis/py.typed +0 -0
  102. {axis-58 → axis-60}/axis/rtsp.py +0 -0
  103. {axis-58 → axis-60}/axis/stream_manager.py +0 -0
  104. {axis-58 → axis-60}/axis.egg-info/dependency_links.txt +0 -0
  105. {axis-58 → axis-60}/axis.egg-info/entry_points.txt +0 -0
  106. {axis-58 → axis-60}/axis.egg-info/top_level.txt +0 -0
  107. {axis-58 → axis-60}/setup.cfg +0 -0
  108. {axis-58 → axis-60}/tests/test_device.py +0 -0
  109. {axis-58 → axis-60}/tests/test_event.py +0 -0
  110. {axis-58 → axis-60}/tests/test_rtsp.py +0 -0
  111. {axis-58 → axis-60}/tests/test_stream_manager.py +0 -0
axis-60/PKG-INFO ADDED
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.1
2
+ Name: axis
3
+ Version: 60
4
+ Summary: A Python library for communicating with devices from Axis Communications
5
+ Author-email: Robert Svensson <Kane610@users.noreply.github.com>
6
+ License: MIT
7
+ Project-URL: Source Code, https://github.com/Kane610/axis
8
+ Project-URL: Bug Reports, https://github.com/Kane610/axis/issues
9
+ Project-URL: Forum, https://community.home-assistant.io/t/axis-camera-component/
10
+ Keywords: axis,vapix,homeassistant
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Topic :: Home Automation
17
+ Requires-Python: >=3.11.0
18
+ Description-Content-Type: text/markdown
19
+ Provides-Extra: requirements
20
+ Provides-Extra: requirements_test
21
+ Provides-Extra: requirements_dev
22
+ License-File: LICENSE
23
+
24
+ Python project to set up a connection towards Axis Communications devices and to subscribe to specific events on the metadatastream.
@@ -22,7 +22,7 @@ async def axis_device(
22
22
  """Create a Axis device."""
23
23
  session = AsyncClient(verify=False)
24
24
  device = axis.device.AxisDevice(
25
- axis.configuration.Configuration(
25
+ axis.models.configuration.Configuration(
26
26
  session, host, port=port, username=username, password=password
27
27
  )
28
28
  )
@@ -1,9 +1,9 @@
1
1
  """Python library to enable Axis devices to integrate with Home Assistant."""
2
2
 
3
- from .configuration import Configuration
4
- from .event_stream import EventManager
3
+ from .interfaces.event_manager import EventManager
4
+ from .interfaces.vapix import Vapix
5
+ from .models.configuration import Configuration
5
6
  from .stream_manager import StreamManager
6
- from .vapix.vapix import Vapix
7
7
 
8
8
 
9
9
  class AxisDevice:
@@ -14,7 +14,7 @@ class AxisDevice:
14
14
  self.config = configuration
15
15
  self.vapix = Vapix(self)
16
16
  self.stream = StreamManager(self)
17
- self.event = EventManager(self)
17
+ self.event = EventManager()
18
18
 
19
19
  def enable_events(self) -> None:
20
20
  """Enable events for stream."""
@@ -11,11 +11,11 @@ from collections.abc import (
11
11
  )
12
12
  from typing import TYPE_CHECKING, Generic, final
13
13
 
14
- from ...errors import Forbidden, PathNotFound, Unauthorized
14
+ from ..errors import Forbidden, PathNotFound, Unauthorized
15
15
 
16
16
  if TYPE_CHECKING:
17
17
  from ..models.api_discovery import ApiId
18
- from ..vapix import Vapix
18
+ from .vapix import Vapix
19
19
 
20
20
  from ..models.api import ApiItemT
21
21
 
@@ -2,13 +2,9 @@
2
2
 
3
3
  from collections.abc import Callable
4
4
  import logging
5
- from typing import TYPE_CHECKING, Any
6
-
7
- from .models.event import Event, EventOperation, EventTopic
8
-
9
- if TYPE_CHECKING:
10
- from .device import AxisDevice
5
+ from typing import Any
11
6
 
7
+ from ..models.event import Event, EventOperation, EventTopic
12
8
 
13
9
  SubscriptionCallback = Callable[[Event], None]
14
10
  SubscriptionType = tuple[
@@ -30,9 +26,8 @@ LOGGER = logging.getLogger(__name__)
30
26
  class EventManager:
31
27
  """Initialize new events and update states of existing events."""
32
28
 
33
- def __init__(self, device: "AxisDevice") -> None:
29
+ def __init__(self) -> None:
34
30
  """Ready information about events."""
35
- self.device = device
36
31
  self._known_topics: set[str] = set()
37
32
  self._subscribers: dict[str, list[SubscriptionType]] = {ID_FILTER_ALL: []}
38
33
 
@@ -6,6 +6,7 @@ import orjson
6
6
 
7
7
  from ..models.api_discovery import ApiId
8
8
  from ..models.mqtt import (
9
+ API_VERSION,
9
10
  ActivateClientRequest,
10
11
  ClientConfig,
11
12
  ClientConfigStatus,
@@ -21,8 +22,6 @@ from ..models.mqtt import (
21
22
  )
22
23
  from .api_handler import ApiHandler
23
24
 
24
- API_VERSION = "1.0"
25
-
26
25
  DEFAULT_TOPICS = ["//."]
27
26
 
28
27
 
@@ -56,41 +55,36 @@ class MqttClientHandler(ApiHandler[Any]):
56
55
 
57
56
  async def configure_client(self, client_config: ClientConfig) -> None:
58
57
  """Configure MQTT Client."""
59
- discovery_item = self.vapix.api_discovery[self.api_id]
60
58
  await self.vapix.api_request(
61
59
  ConfigureClientRequest(
62
- api_version=discovery_item.version, client_config=client_config
60
+ api_version=self.default_api_version, client_config=client_config
63
61
  )
64
62
  )
65
63
 
66
64
  async def activate(self) -> None:
67
65
  """Activate MQTT Client."""
68
- discovery_item = self.vapix.api_discovery[self.api_id]
69
66
  await self.vapix.api_request(
70
- ActivateClientRequest(api_version=discovery_item.version)
67
+ ActivateClientRequest(api_version=self.default_api_version)
71
68
  )
72
69
 
73
70
  async def deactivate(self) -> None:
74
71
  """Deactivate MQTT Client."""
75
- discovery_item = self.vapix.api_discovery[self.api_id]
76
72
  await self.vapix.api_request(
77
- DeactivateClientRequest(api_version=discovery_item.version)
73
+ DeactivateClientRequest(api_version=self.default_api_version)
78
74
  )
79
75
 
80
76
  async def get_client_status(self) -> ClientConfigStatus:
81
77
  """Get MQTT Client status."""
82
- discovery_item = self.vapix.api_discovery[self.api_id]
83
78
  bytes_data = await self.vapix.api_request(
84
- GetClientStatusRequest(api_version=discovery_item.version)
79
+ GetClientStatusRequest(api_version=self.default_api_version)
85
80
  )
86
81
  response = GetClientStatusResponse.decode(bytes_data)
87
82
  return response.data
88
83
 
89
84
  async def get_event_publication_config(self) -> EventPublicationConfig:
90
85
  """Get MQTT Client event publication config."""
91
- discovery_item = self.vapix.api_discovery[self.api_id]
92
86
  bytes_data = await self.vapix.api_request(
93
- GetEventPublicationConfigRequest(api_version=discovery_item.version)
87
+ GetEventPublicationConfigRequest(api_version=self.default_api_version)
94
88
  )
95
89
  response = GetEventPublicationConfigResponse.decode(bytes_data)
96
90
  return response.data
@@ -99,13 +93,12 @@ class MqttClientHandler(ApiHandler[Any]):
99
93
  self, topics: list[str] = DEFAULT_TOPICS
100
94
  ) -> None:
101
95
  """Configure MQTT Client event publication."""
102
- discovery_item = self.vapix.api_discovery[self.api_id]
103
96
  event_filters = EventFilter.from_list(
104
97
  [{"topicFilter": topic} for topic in topics]
105
98
  )
106
99
  config = EventPublicationConfig(event_filter_list=event_filters)
107
100
  await self.vapix.api_request(
108
101
  ConfigureEventPublicationRequest(
109
- api_version=discovery_item.version, config=config
102
+ api_version=self.default_api_version, config=config
110
103
  )
111
104
  )
@@ -14,7 +14,7 @@ from .ptz import PtzParameterHandler
14
14
  from .stream_profile import StreamProfileParameterHandler
15
15
 
16
16
  if TYPE_CHECKING:
17
- from ...vapix import Vapix
17
+ from ..vapix import Vapix
18
18
 
19
19
 
20
20
  class Params(ApiHandler[Any]):
@@ -1,5 +1,7 @@
1
1
  """Python library to enable Axis devices to integrate with Home Assistant."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import asyncio
4
6
  import logging
5
7
  from typing import TYPE_CHECKING, Any
@@ -7,36 +9,36 @@ from typing import TYPE_CHECKING, Any
7
9
  import httpx
8
10
 
9
11
  from ..errors import RequestError, raise_error
10
- from .interfaces.api_discovery import ApiDiscoveryHandler
11
- from .interfaces.api_handler import ApiHandler
12
- from .interfaces.applications import (
12
+ from ..models.api import ApiRequest
13
+ from ..models.pwdgrp_cgi import SecondaryGroup
14
+ from ..models.stream_profile import StreamProfile
15
+ from .api_discovery import ApiDiscoveryHandler
16
+ from .api_handler import ApiHandler
17
+ from .applications import (
13
18
  ApplicationsHandler,
14
19
  )
15
- from .interfaces.applications.fence_guard import FenceGuardHandler
16
- from .interfaces.applications.loitering_guard import (
20
+ from .applications.fence_guard import FenceGuardHandler
21
+ from .applications.loitering_guard import (
17
22
  LoiteringGuardHandler,
18
23
  )
19
- from .interfaces.applications.motion_guard import MotionGuardHandler
20
- from .interfaces.applications.object_analytics import (
24
+ from .applications.motion_guard import MotionGuardHandler
25
+ from .applications.object_analytics import (
21
26
  ObjectAnalyticsHandler,
22
27
  )
23
- from .interfaces.applications.vmd4 import Vmd4Handler
24
- from .interfaces.basic_device_info import BasicDeviceInfoHandler
25
- from .interfaces.event_instances import EventInstanceHandler
26
- from .interfaces.light_control import LightHandler
27
- from .interfaces.mqtt import MqttClientHandler
28
- from .interfaces.parameters.param_cgi import Params
29
- from .interfaces.pir_sensor_configuration import PirSensorConfigurationHandler
30
- from .interfaces.port_cgi import Ports
31
- from .interfaces.port_management import IoPortManagement
32
- from .interfaces.ptz import PtzControl
33
- from .interfaces.pwdgrp_cgi import Users
34
- from .interfaces.stream_profiles import StreamProfilesHandler
35
- from .interfaces.user_groups import UserGroups
36
- from .interfaces.view_areas import ViewAreaHandler
37
- from .models.api import ApiRequest
38
- from .models.pwdgrp_cgi import SecondaryGroup
39
- from .models.stream_profile import StreamProfile
28
+ from .applications.vmd4 import Vmd4Handler
29
+ from .basic_device_info import BasicDeviceInfoHandler
30
+ from .event_instances import EventInstanceHandler
31
+ from .light_control import LightHandler
32
+ from .mqtt import MqttClientHandler
33
+ from .parameters.param_cgi import Params
34
+ from .pir_sensor_configuration import PirSensorConfigurationHandler
35
+ from .port_cgi import Ports
36
+ from .port_management import IoPortManagement
37
+ from .ptz import PtzControl
38
+ from .pwdgrp_cgi import Users
39
+ from .stream_profiles import StreamProfilesHandler
40
+ from .user_groups import UserGroups
41
+ from .view_areas import ViewAreaHandler
40
42
 
41
43
  if TYPE_CHECKING:
42
44
  from ..device import AxisDevice
@@ -49,7 +51,7 @@ TIME_OUT = 15
49
51
  class Vapix:
50
52
  """Vapix parameter request."""
51
53
 
52
- def __init__(self, device: "AxisDevice") -> None:
54
+ def __init__(self, device: AxisDevice) -> None:
53
55
  """Store local reference to device config."""
54
56
  self.device = device
55
57
  self.auth = httpx.DigestAuth(device.config.username, device.config.password)
@@ -5,8 +5,8 @@ from typing import Any, Self
5
5
 
6
6
  import xmltodict
7
7
 
8
- from ...models.event import traverse
9
8
  from .api import ApiItem, ApiRequest, ApiResponse
9
+ from .event import traverse
10
10
 
11
11
  EVENT_INSTANCE = (
12
12
  "http://www.w3.org/2003/05/soap-envelope:Envelope",
@@ -1,5 +1,7 @@
1
1
  """Light Control API data model."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  from dataclasses import dataclass
4
6
  from typing import NotRequired, Self
5
7
 
@@ -225,14 +227,14 @@ class Range:
225
227
  high: int
226
228
 
227
229
  @classmethod
228
- def from_dict(cls, data: RangeT) -> "Range":
230
+ def from_dict(cls, data: RangeT) -> Self:
229
231
  """Create range object from dict."""
230
- return Range(low=data["low"], high=data["high"])
232
+ return cls(low=data["low"], high=data["high"])
231
233
 
232
234
  @classmethod
233
- def from_list(cls, data: list[RangeT]) -> list["Range"]:
235
+ def from_list(cls, data: list[RangeT]) -> list[Self]:
234
236
  """Create range object from dict."""
235
- return [Range.from_dict(range) for range in data]
237
+ return [cls.from_dict(range) for range in data]
236
238
 
237
239
 
238
240
  @dataclass
@@ -277,7 +279,7 @@ class GetLightInformationResponse(ApiResponse[dict[str, LightInformation]]):
277
279
  api_version=data["apiVersion"],
278
280
  context=data["context"],
279
281
  method=data["method"],
280
- data=LightInformation.decode_to_dict(data["data"]["items"]),
282
+ data=LightInformation.decode_to_dict(data.get("data", {}).get("items", [])),
281
283
  )
282
284
 
283
285
 
@@ -294,9 +296,9 @@ class ServiceCapabilities:
294
296
  day_night_synchronize_support: bool
295
297
 
296
298
  @classmethod
297
- def from_dict(cls, data: ServiceCapabilitiesT) -> "ServiceCapabilities":
299
+ def from_dict(cls, data: ServiceCapabilitiesT) -> Self:
298
300
  """Create service capabilities object from dict."""
299
- return ServiceCapabilities(
301
+ return cls(
300
302
  automatic_intensity_support=data["automaticIntensitySupport"],
301
303
  manual_intensity_support=data["manualIntensitySupport"],
302
304
  get_current_intensity_support=data["getCurrentIntensitySupport"],
@@ -1,5 +1,7 @@
1
1
  """MQTT Client api."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  from dataclasses import dataclass
4
6
  import enum
5
7
  from typing import Literal, NotRequired, Self
@@ -24,7 +26,7 @@ class MessageT(TypedDict):
24
26
 
25
27
  useDefault: bool
26
28
  message: NotRequired[str]
27
- qos: NotRequired[int]
29
+ qos: NotRequired[Literal[0, 1, 2]]
28
30
  retain: NotRequired[bool]
29
31
  topic: NotRequired[str]
30
32
 
@@ -70,7 +72,7 @@ class ConfigT(TypedDict):
70
72
  class StatusT(TypedDict):
71
73
  """Represent a status object."""
72
74
 
73
- connectionStatus: Literal["connected", "disconnected"]
75
+ connectionStatus: Literal["connected", "disconnected", "not connected"]
74
76
  state: Literal["active", "inactive"]
75
77
 
76
78
 
@@ -149,6 +151,14 @@ class ClientConnectionState(enum.StrEnum):
149
151
  CONNECTED = "connected"
150
152
  DISCONNECTED = "disconnected"
151
153
 
154
+ @classmethod
155
+ def _missing_(cls, value: object) -> ClientConnectionState:
156
+ """Set default enum member if an unknown value is provided.
157
+
158
+ Some firmwares report "not connected" instead of "disconnected".
159
+ """
160
+ return cls.DISCONNECTED
161
+
152
162
 
153
163
  class ServerProtocol(enum.StrEnum):
154
164
  """Connection protocols used in the server configuration."""
@@ -164,10 +174,35 @@ class Message:
164
174
  """Message description."""
165
175
 
166
176
  use_default: bool = True
177
+ """Specifies if the default configuration should be used.
178
+
179
+ If set to true, other options in this parameter will be discarded.
180
+ If set to false, topic, messages, retained and qos options are required and used.
181
+ """
167
182
  topic: str | None = None
183
+ """The topic that should be used with the message."""
168
184
  message: str | None = None
185
+ """The message that should be used with the message."""
169
186
  retain: bool | None = None
170
- qos: int | None = None
187
+ """The retained option that should be used with the message."""
188
+ qos: Literal[0, 1, 2] | None = None
189
+ """The QoS option that should be used with the message."""
190
+
191
+ @classmethod
192
+ def from_dict(cls, data: MessageT) -> Self:
193
+ """Create message object from dict."""
194
+ return cls(
195
+ use_default=data["useDefault"],
196
+ topic=data.get("topic"),
197
+ message=data.get("message"),
198
+ retain=data.get("retain"),
199
+ qos=data.get("qos"),
200
+ )
201
+
202
+ @classmethod
203
+ def from_dict_or_none(cls, data: MessageT | None) -> Self | None:
204
+ """Create class instance if data is not None."""
205
+ return cls.from_dict(data) if data is not None else None
171
206
 
172
207
  def to_dict(self) -> MessageT:
173
208
  """Create json dict from object."""
@@ -188,15 +223,42 @@ class Server:
188
223
  """Represent server config."""
189
224
 
190
225
  host: str
226
+ """The Broker location, i.e. the Hostname or IP address."""
191
227
  protocol: ServerProtocol = ServerProtocol.TCP
228
+ """Contains the protocols used by params.server.
229
+
230
+ Possible values are:
231
+ - tcp: MQTT over TCP
232
+ - ssl: MQTT over SSL
233
+ - ws: MQTT over Websocket
234
+ - wss: MQTT over Websocket Secure
235
+ """
192
236
  alpn_protocol: str | None = None
237
+ """The ALPN protocol that should be used if the selected protocol was ssl or wss.
238
+
239
+ If the string value is empty the ALPN will not be used.
240
+ Default value is empty and the maximum length of the protocol is 255 bytes.
241
+ """
193
242
  basepath: str | None = None
243
+ """The path that should be used as a suffix for the constructed URL.
244
+
245
+ Will only be used for the ws and wss connections.
246
+ The default value is empty string.
247
+ """
194
248
  port: int | None = None
249
+ """The port that should be used.
250
+
251
+ Default values for the protocols are:
252
+ - tcp: 1883
253
+ - ssl: 8883
254
+ - ws: 80
255
+ - wss: 443
256
+ """
195
257
 
196
258
  @classmethod
197
- def from_dict(cls, data: ServerT) -> "Server":
259
+ def from_dict(cls, data: ServerT) -> Self:
198
260
  """Create server object from dict."""
199
- return Server(
261
+ return cls(
200
262
  host=data["host"],
201
263
  protocol=ServerProtocol(data["protocol"]),
202
264
  alpn_protocol=data.get("alpnProtocol"),
@@ -221,8 +283,31 @@ class Ssl:
221
283
  """Represent SSL config."""
222
284
 
223
285
  validate_server_cert: bool = False
286
+ """Specifies if the server certificate shall be validated."""
224
287
  ca_cert_id: str | None = None
288
+ """Specifies the CA Certificate that should be used to validate the server certificate.
289
+
290
+ The certificates are managed through the user interface or via ONVIF services.
291
+ """
225
292
  client_cert_id: str | None = None
293
+ """Specifies the client certificate and key that should be used.
294
+
295
+ The certificates are managed through the user interface or via ONVIF services.
296
+ """
297
+
298
+ @classmethod
299
+ def from_dict(cls, data: SslT) -> Self:
300
+ """Create client status object from dict."""
301
+ return cls(
302
+ validate_server_cert=data["validateServerCert"],
303
+ ca_cert_id=data.get("CACertID"),
304
+ client_cert_id=data.get("clientCertID"),
305
+ )
306
+
307
+ @classmethod
308
+ def from_dict_or_none(cls, data: SslT | None) -> Self | None:
309
+ """Create class instance if data is not None."""
310
+ return cls.from_dict(data) if data is not None else None
226
311
 
227
312
  def to_dict(self) -> SslT:
228
313
  """Create json dict from object."""
@@ -239,23 +324,90 @@ class ClientConfig:
239
324
  """Represent client config."""
240
325
 
241
326
  server: Server
327
+ """Contains the address and protocol related information."""
242
328
  activate_on_reboot: bool | None = None
243
329
  auto_reconnect: bool | None = None
330
+ """Specifies if the client should reconnect on an unintentional disconnect."""
244
331
  clean_session: bool | None = None
332
+ """This parameter controls the behavior during connection and disconnection time.
333
+
334
+ Affects both the client and the server when this parameters is true,
335
+ the state information is discarded when the client and server change state.
336
+ Setting the parameter to false means that the state information is kept.
337
+ """
245
338
  client_id: str | None = None
339
+ """The client identifier sent to the server when the client connects to it."""
246
340
  connect_message: Message | None = None
341
+ """Specifies if a message should be sent when a connection is established.
342
+
343
+ Contains options related to connect announcements.
344
+ If this object is not defined this message won’t be sent.
345
+ """
247
346
  connect_timeout: int | None = None
347
+ """The timed interval (in seconds) to allow a connect to finish.
348
+
349
+ The default value is 60.
350
+ """
351
+ device_topic_prefix: str | None = None
352
+ """Specifies a prefix on MQTT topics in various scenarios.
353
+
354
+ Such as when you want to configure the translation of events into MQTT messages
355
+ or prefix all published MQTT messages with a common prefix.
356
+ The default value is axis/{device serial number}.
357
+ """
248
358
  disconnect_message: Message | None = None
359
+ """Specifies if a message should be sent when the client is manually disconnected.
360
+
361
+ Contains options related to manual disconnect announcements.
362
+ If this object is not defined this message won’t be sent.
363
+ This message should not be confused with LWT,
364
+ as it is used when the connection is lost and managed by the broker.
365
+ """
249
366
  keep_alive_interval: int | None = None
367
+ """Defines the maximum time (in seconds).
368
+
369
+ Time intervalthat should pass without communication between the client and server.
370
+ At least one message will be sent over the network by the client
371
+ during each keep alive period and the interval makes it possible to detect
372
+ when the server is no longer available without having to
373
+ wait for the TCP/IP timeout.
374
+ The default value is 60."""
250
375
  last_will_testament: Message | None = None
376
+ """Contains the options related to LWT.
377
+
378
+ If LWT is not required, this parameter is not included.
379
+ """
251
380
  password: str | None = None
381
+ """The password that should be used for authentication."""
252
382
  ssl: Ssl | None = None
383
+ """Contains the options related to the SSL connection.
384
+
385
+ This object should only be present if the connection type is ssl or wss.
386
+ """
253
387
  username: str | None = None
388
+ """The user name that should be used for authentication and authorization."""
254
389
 
255
390
  @classmethod
256
- def from_dict(cls, data: ConfigT) -> "ClientConfig":
391
+ def from_dict(cls, data: ConfigT) -> Self:
257
392
  """Create client status object from dict."""
258
- return ClientConfig(server=Server.from_dict(data["server"]))
393
+ return cls(
394
+ server=Server.from_dict(data["server"]),
395
+ activate_on_reboot=data.get("activateOnReboot"),
396
+ auto_reconnect=data.get("autoReconnect"),
397
+ clean_session=data.get("cleanSession"),
398
+ client_id=data.get("clientId"),
399
+ connect_message=Message.from_dict_or_none(data.get("connectMessage")),
400
+ connect_timeout=data.get("connectTimeout"),
401
+ device_topic_prefix=data.get("deviceTopicPrefix"),
402
+ disconnect_message=Message.from_dict_or_none(data.get("disconnectMessage")),
403
+ keep_alive_interval=data.get("keepAliveInterval"),
404
+ last_will_testament=Message.from_dict_or_none(
405
+ data.get("lastWillTestament")
406
+ ),
407
+ password=data.get("password"),
408
+ ssl=Ssl.from_dict_or_none(data.get("ssl")),
409
+ username=data.get("username"),
410
+ )
259
411
 
260
412
  def to_dict(self) -> ConfigT:
261
413
  """Create json dict from object."""
@@ -293,14 +445,19 @@ class ClientStatus:
293
445
 
294
446
  connection_status: ClientConnectionState
295
447
  state: ClientState
448
+ active: bool
449
+ """The current state of the client. Possible values are active and inactive."""
450
+ connected: bool
451
+ """The current connection state of your client. Possible values are connected, disconnected."""
296
452
 
297
453
  @classmethod
298
- def from_dict(cls, data: StatusT) -> "ClientStatus":
454
+ def from_dict(cls, data: StatusT) -> Self:
299
455
  """Create client status object from dict."""
300
- # Note to investigate closer, documentation say lower case.
301
- return ClientStatus(
456
+ return cls(
302
457
  connection_status=ClientConnectionState(data["connectionStatus"].lower()),
303
458
  state=ClientState(data["state"].lower()),
459
+ active=data["state"] == "active",
460
+ connected=data["connectionStatus"] == "connected",
304
461
  )
305
462
 
306
463
 
@@ -309,12 +466,14 @@ class ClientConfigStatus:
309
466
  """GetClientStatus response."""
310
467
 
311
468
  config: ClientConfig
469
+ """The Config of the MQTT client."""
312
470
  status: ClientStatus
471
+ """The Status of the MQTT client."""
313
472
 
314
473
  @classmethod
315
- def from_dict(cls, data: ClientStatusDataT) -> "ClientConfigStatus":
474
+ def from_dict(cls, data: ClientStatusDataT) -> Self:
316
475
  """Create client config status object from dict."""
317
- return ClientConfigStatus(
476
+ return cls(
318
477
  config=ClientConfig.from_dict(data["config"]),
319
478
  status=ClientStatus.from_dict(data["status"]),
320
479
  )
@@ -329,18 +488,18 @@ class EventFilter:
329
488
  retain: str | None = None
330
489
 
331
490
  @classmethod
332
- def from_dict(cls, data: EventFilterT) -> "EventFilter":
491
+ def from_dict(cls, data: EventFilterT) -> Self:
333
492
  """Create event filter object from dict."""
334
- return EventFilter(
493
+ return cls(
335
494
  topic_filter=data["topicFilter"],
336
495
  qos=data.get("qos"),
337
496
  retain=data.get("retain"),
338
497
  )
339
498
 
340
499
  @classmethod
341
- def from_list(cls, data: list[EventFilterT]) -> "list[EventFilter]":
500
+ def from_list(cls, data: list[EventFilterT]) -> list[Self]:
342
501
  """Create event filter object from dict."""
343
- return [EventFilter.from_dict(item) for item in data]
502
+ return [cls.from_dict(item) for item in data]
344
503
 
345
504
  def to_dict(self) -> EventFilterT:
346
505
  """Create json dict from object."""
@@ -352,7 +511,7 @@ class EventFilter:
352
511
  return data
353
512
 
354
513
  @classmethod
355
- def to_list(cls, data: "list[EventFilter]") -> list[EventFilterT]:
514
+ def to_list(cls, data: list[EventFilter]) -> list[EventFilterT]:
356
515
  """Create json list from object."""
357
516
  return [item.to_dict() for item in data]
358
517
 
@@ -369,9 +528,9 @@ class EventPublicationConfig:
369
528
  event_filter_list: list[EventFilter] | None = None
370
529
 
371
530
  @classmethod
372
- def from_dict(cls, data: EventPublicationConfigT) -> "EventPublicationConfig":
531
+ def from_dict(cls, data: EventPublicationConfigT) -> Self:
373
532
  """Create client config status object from dict."""
374
- return EventPublicationConfig(
533
+ return cls(
375
534
  topic_prefix=data["topicPrefix"],
376
535
  custom_topic_prefix=data["customTopicPrefix"],
377
536
  append_event_topic=data["appendEventTopic"],
@@ -188,8 +188,9 @@ class PropertyParam(ParamItem):
188
188
  .get("PTZ", {})
189
189
  .get("Presets", {})
190
190
  .get("Version", False),
191
- embedded_development=data["EmbeddedDevelopment"]["Version"],
192
- # embedded_development=data.get("EmbeddedDevelopment_Version", "0.0"),
191
+ embedded_development=data.get("EmbeddedDevelopment", {}).get(
192
+ "Version", "0.0"
193
+ ),
193
194
  firmware_build_date=data["Firmware"]["BuildDate"],
194
195
  firmware_build_number=data["Firmware"]["BuildNumber"],
195
196
  firmware_version=data["Firmware"]["Version"],