axis 63__tar.gz → 65__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.
- axis-65/PKG-INFO +43 -0
- {axis-63 → axis-65}/axis/__main__.py +7 -2
- {axis-63 → axis-65}/axis/interfaces/mqtt.py +1 -1
- {axis-63 → axis-65}/axis/interfaces/parameters/param_cgi.py +15 -2
- {axis-63 → axis-65}/axis/interfaces/vapix.py +7 -10
- {axis-63 → axis-65}/axis/models/configuration.py +1 -0
- {axis-63 → axis-65}/axis/models/event_instance.py +1 -1
- {axis-63 → axis-65}/axis/models/parameters/stream_profile.py +1 -1
- {axis-63 → axis-65}/axis/stream_manager.py +4 -3
- axis-65/axis.egg-info/PKG-INFO +43 -0
- axis-65/axis.egg-info/requires.txt +24 -0
- {axis-63 → axis-65}/pyproject.toml +16 -18
- {axis-63 → axis-65}/tests/test_configuration.py +2 -0
- {axis-63 → axis-65}/tests/test_port_cgi.py +5 -5
- {axis-63 → axis-65}/tests/test_stream_manager.py +24 -0
- {axis-63 → axis-65}/tests/test_vapix.py +58 -17
- axis-63/PKG-INFO +0 -24
- axis-63/axis.egg-info/PKG-INFO +0 -24
- axis-63/axis.egg-info/requires.txt +0 -24
- {axis-63 → axis-65}/LICENSE +0 -0
- {axis-63 → axis-65}/README.md +0 -0
- {axis-63 → axis-65}/axis/__init__.py +0 -0
- {axis-63 → axis-65}/axis/device.py +0 -0
- {axis-63 → axis-65}/axis/errors.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/__init__.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/api_discovery.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/api_handler.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/applications/__init__.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/applications/application_handler.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/applications/applications.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/applications/fence_guard.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/applications/loitering_guard.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/applications/motion_guard.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/applications/object_analytics.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/applications/vmd4.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/basic_device_info.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/event_instances.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/event_manager.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/light_control.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/parameters/__init__.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/parameters/brand.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/parameters/image.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/parameters/io_port.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/parameters/param_handler.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/parameters/properties.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/parameters/ptz.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/parameters/stream_profile.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/pir_sensor_configuration.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/port_cgi.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/port_management.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/ptz.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/pwdgrp_cgi.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/stream_profiles.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/user_groups.py +0 -0
- {axis-63 → axis-65}/axis/interfaces/view_areas.py +0 -0
- {axis-63 → axis-65}/axis/models/__init__.py +0 -0
- {axis-63 → axis-65}/axis/models/api.py +0 -0
- {axis-63 → axis-65}/axis/models/api_discovery.py +0 -0
- {axis-63 → axis-65}/axis/models/applications/__init__.py +0 -0
- {axis-63 → axis-65}/axis/models/applications/application.py +0 -0
- {axis-63 → axis-65}/axis/models/applications/fence_guard.py +0 -0
- {axis-63 → axis-65}/axis/models/applications/loitering_guard.py +0 -0
- {axis-63 → axis-65}/axis/models/applications/motion_guard.py +0 -0
- {axis-63 → axis-65}/axis/models/applications/object_analytics.py +0 -0
- {axis-63 → axis-65}/axis/models/applications/vmd4.py +0 -0
- {axis-63 → axis-65}/axis/models/basic_device_info.py +0 -0
- {axis-63 → axis-65}/axis/models/event.py +0 -0
- {axis-63 → axis-65}/axis/models/light_control.py +0 -0
- {axis-63 → axis-65}/axis/models/mqtt.py +0 -0
- {axis-63 → axis-65}/axis/models/parameters/__init__.py +0 -0
- {axis-63 → axis-65}/axis/models/parameters/brand.py +0 -0
- {axis-63 → axis-65}/axis/models/parameters/image.py +0 -0
- {axis-63 → axis-65}/axis/models/parameters/io_port.py +0 -0
- {axis-63 → axis-65}/axis/models/parameters/param_cgi.py +0 -0
- {axis-63 → axis-65}/axis/models/parameters/properties.py +0 -0
- {axis-63 → axis-65}/axis/models/parameters/ptz.py +0 -0
- {axis-63 → axis-65}/axis/models/pir_sensor_configuration.py +0 -0
- {axis-63 → axis-65}/axis/models/port_cgi.py +0 -0
- {axis-63 → axis-65}/axis/models/port_management.py +0 -0
- {axis-63 → axis-65}/axis/models/ptz_cgi.py +0 -0
- {axis-63 → axis-65}/axis/models/pwdgrp_cgi.py +0 -0
- {axis-63 → axis-65}/axis/models/stream_profile.py +0 -0
- {axis-63 → axis-65}/axis/models/user_group.py +0 -0
- {axis-63 → axis-65}/axis/models/view_area.py +0 -0
- {axis-63 → axis-65}/axis/py.typed +0 -0
- {axis-63 → axis-65}/axis/rtsp.py +0 -0
- {axis-63 → axis-65}/axis.egg-info/SOURCES.txt +0 -0
- {axis-63 → axis-65}/axis.egg-info/dependency_links.txt +0 -0
- {axis-63 → axis-65}/axis.egg-info/entry_points.txt +0 -0
- {axis-63 → axis-65}/axis.egg-info/top_level.txt +0 -0
- {axis-63 → axis-65}/setup.cfg +0 -0
- {axis-63 → axis-65}/tests/test_api_discovery.py +0 -0
- {axis-63 → axis-65}/tests/test_api_handler.py +0 -0
- {axis-63 → axis-65}/tests/test_basic_device_info.py +0 -0
- {axis-63 → axis-65}/tests/test_device.py +0 -0
- {axis-63 → axis-65}/tests/test_event.py +0 -0
- {axis-63 → axis-65}/tests/test_event_instances.py +0 -0
- {axis-63 → axis-65}/tests/test_event_stream.py +0 -0
- {axis-63 → axis-65}/tests/test_light_control.py +0 -0
- {axis-63 → axis-65}/tests/test_mqtt.py +0 -0
- {axis-63 → axis-65}/tests/test_pir_sensor_configuration.py +0 -0
- {axis-63 → axis-65}/tests/test_port_management.py +0 -0
- {axis-63 → axis-65}/tests/test_ptz.py +0 -0
- {axis-63 → axis-65}/tests/test_pwdgrp_cgi.py +0 -0
- {axis-63 → axis-65}/tests/test_rtsp.py +0 -0
- {axis-63 → axis-65}/tests/test_stream_profiles.py +0 -0
- {axis-63 → axis-65}/tests/test_user_groups.py +0 -0
- {axis-63 → axis-65}/tests/test_view_areas.py +0 -0
axis-65/PKG-INFO
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: axis
|
|
3
|
+
Version: 65
|
|
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.12
|
|
16
|
+
Classifier: Topic :: Home Automation
|
|
17
|
+
Requires-Python: >=3.12.0
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: faust-cchardet>=2.1.18
|
|
21
|
+
Requires-Dist: httpx>=0.26
|
|
22
|
+
Requires-Dist: orjson>3.9
|
|
23
|
+
Requires-Dist: packaging>23
|
|
24
|
+
Requires-Dist: xmltodict>=0.13.0
|
|
25
|
+
Provides-Extra: requirements
|
|
26
|
+
Requires-Dist: httpx==0.28.1; extra == "requirements"
|
|
27
|
+
Requires-Dist: orjson==3.11.1; extra == "requirements"
|
|
28
|
+
Requires-Dist: packaging==25.0; extra == "requirements"
|
|
29
|
+
Requires-Dist: xmltodict==0.14.2; extra == "requirements"
|
|
30
|
+
Provides-Extra: requirements-test
|
|
31
|
+
Requires-Dist: mypy==1.17.1; extra == "requirements-test"
|
|
32
|
+
Requires-Dist: pytest==8.4.1; extra == "requirements-test"
|
|
33
|
+
Requires-Dist: pytest-aiohttp==1.1.0; extra == "requirements-test"
|
|
34
|
+
Requires-Dist: pytest-asyncio==0.26.0; extra == "requirements-test"
|
|
35
|
+
Requires-Dist: pytest-cov==6.2.1; extra == "requirements-test"
|
|
36
|
+
Requires-Dist: respx==0.22.0; extra == "requirements-test"
|
|
37
|
+
Requires-Dist: ruff==0.11.11; extra == "requirements-test"
|
|
38
|
+
Requires-Dist: types-xmltodict==v0.14.0.20241009; extra == "requirements-test"
|
|
39
|
+
Provides-Extra: requirements-dev
|
|
40
|
+
Requires-Dist: pre-commit==4.2.0; extra == "requirements-dev"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
Python project to set up a connection towards Axis Communications devices and to subscribe to specific events on the metadatastream.
|
|
@@ -17,13 +17,18 @@ def event_handler(event: axis.models.event.Event) -> None:
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
async def axis_device(
|
|
20
|
-
host: str, port: int, username: str, password: str
|
|
20
|
+
host: str, port: int, username: str, password: str, is_companion: bool = False
|
|
21
21
|
) -> axis.device.AxisDevice:
|
|
22
22
|
"""Create a Axis device."""
|
|
23
23
|
session = AsyncClient(verify=False) # noqa: S501
|
|
24
24
|
device = axis.device.AxisDevice(
|
|
25
25
|
axis.models.configuration.Configuration(
|
|
26
|
-
session,
|
|
26
|
+
session,
|
|
27
|
+
host,
|
|
28
|
+
port=port,
|
|
29
|
+
username=username,
|
|
30
|
+
password=password,
|
|
31
|
+
is_companion=is_companion,
|
|
27
32
|
)
|
|
28
33
|
)
|
|
29
34
|
|
|
@@ -25,7 +25,7 @@ from .api_handler import ApiHandler
|
|
|
25
25
|
DEFAULT_TOPICS = ["//."]
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
def mqtt_json_to_event(msg: bytes | str) -> dict[str, Any]:
|
|
28
|
+
def mqtt_json_to_event(msg: bytes | bytearray | memoryview | str) -> dict[str, Any]:
|
|
29
29
|
"""Convert JSON message from MQTT to event format."""
|
|
30
30
|
message = orjson.loads(msg)
|
|
31
31
|
topic = message["topic"].replace("onvif", "tns1").replace("axis", "tnsaxis")
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
"""Axis Vapix parameter management."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import Sequence
|
|
4
|
-
from typing import TYPE_CHECKING, Any
|
|
4
|
+
from typing import TYPE_CHECKING, Any, TypedDict
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
|
|
8
|
+
class _DetectResultType(TypedDict):
|
|
9
|
+
encoding: str
|
|
10
|
+
confidence: float
|
|
11
|
+
|
|
12
|
+
def detect(byte_str: bytes | bytearray) -> _DetectResultType:
|
|
13
|
+
"""Typed interface for chardet detect method."""
|
|
14
|
+
...
|
|
15
|
+
else:
|
|
16
|
+
from cchardet import detect
|
|
5
17
|
|
|
6
18
|
from ...models.api_discovery import ApiId
|
|
7
19
|
from ...models.parameters.param_cgi import ParameterGroup, ParamRequest, params_to_dict
|
|
@@ -36,7 +48,8 @@ class Params(ApiHandler[Any]):
|
|
|
36
48
|
async def _api_request(self, group: ParameterGroup | None = None) -> dict[str, Any]:
|
|
37
49
|
"""Fetch parameter data and convert it into a dictionary."""
|
|
38
50
|
bytes_data = await self.vapix.api_request(ParamRequest(group))
|
|
39
|
-
|
|
51
|
+
encoding = detect(bytes_data)["encoding"] or "utf-8"
|
|
52
|
+
return params_to_dict(bytes_data.decode(encoding=encoding)).get("root") or {}
|
|
40
53
|
|
|
41
54
|
async def _update(self, group: ParameterGroup | None = None) -> Sequence[str]:
|
|
42
55
|
"""Request parameter data and update items."""
|
|
@@ -11,17 +11,11 @@ import httpx
|
|
|
11
11
|
from ..errors import RequestError, raise_error
|
|
12
12
|
from ..models.pwdgrp_cgi import SecondaryGroup
|
|
13
13
|
from .api_discovery import ApiDiscoveryHandler
|
|
14
|
-
from .applications import
|
|
15
|
-
ApplicationsHandler,
|
|
16
|
-
)
|
|
14
|
+
from .applications import ApplicationsHandler
|
|
17
15
|
from .applications.fence_guard import FenceGuardHandler
|
|
18
|
-
from .applications.loitering_guard import
|
|
19
|
-
LoiteringGuardHandler,
|
|
20
|
-
)
|
|
16
|
+
from .applications.loitering_guard import LoiteringGuardHandler
|
|
21
17
|
from .applications.motion_guard import MotionGuardHandler
|
|
22
|
-
from .applications.object_analytics import
|
|
23
|
-
ObjectAnalyticsHandler,
|
|
24
|
-
)
|
|
18
|
+
from .applications.object_analytics import ObjectAnalyticsHandler
|
|
25
19
|
from .applications.vmd4 import Vmd4Handler
|
|
26
20
|
from .basic_device_info import BasicDeviceInfoHandler
|
|
27
21
|
from .event_instances import EventInstanceHandler
|
|
@@ -242,13 +236,16 @@ class Vapix:
|
|
|
242
236
|
|
|
243
237
|
async def api_request(self, api_request: ApiRequest) -> bytes:
|
|
244
238
|
"""Make a request to the device."""
|
|
239
|
+
params = api_request.params or {}
|
|
240
|
+
if self.device.config.is_companion:
|
|
241
|
+
params["Axis-Orig-Sw"] = "true"
|
|
245
242
|
return await self.request(
|
|
246
243
|
method=api_request.method,
|
|
247
244
|
path=api_request.path,
|
|
248
245
|
content=api_request.content,
|
|
249
246
|
data=api_request.data,
|
|
250
247
|
headers=api_request.headers,
|
|
251
|
-
params=
|
|
248
|
+
params=params,
|
|
252
249
|
)
|
|
253
250
|
|
|
254
251
|
async def request(
|
|
@@ -44,7 +44,7 @@ def get_events(data: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
44
44
|
event_list = get_events(value) # Recursive call
|
|
45
45
|
|
|
46
46
|
for event in event_list:
|
|
47
|
-
event["topic"] = f
|
|
47
|
+
event["topic"] = f"{key}/{event['topic']}" # Compose the topic
|
|
48
48
|
events.append(event)
|
|
49
49
|
|
|
50
50
|
return events
|
|
@@ -51,7 +51,7 @@ class StreamProfileParam(ParamItem):
|
|
|
51
51
|
description=profile["Description"],
|
|
52
52
|
parameters=profile["Parameters"],
|
|
53
53
|
)
|
|
54
|
-
for profile in cast(dict[str, ProfileParamT], raw_profiles).values()
|
|
54
|
+
for profile in cast("dict[str, ProfileParamT]", raw_profiles).values()
|
|
55
55
|
]
|
|
56
56
|
|
|
57
57
|
return cls(
|
|
@@ -13,9 +13,7 @@ if TYPE_CHECKING:
|
|
|
13
13
|
|
|
14
14
|
_LOGGER = logging.getLogger(__name__)
|
|
15
15
|
|
|
16
|
-
RTSP_URL =
|
|
17
|
-
"rtsp://{host}/axis-media/media.amp?video={video}&audio={audio}&event={event}"
|
|
18
|
-
)
|
|
16
|
+
RTSP_URL = "rtsp://{host}/axis-media/media.amp?video={video}&audio={audio}&event={event}{axis_orig_sw}"
|
|
19
17
|
|
|
20
18
|
RETRY_TIMER = 15
|
|
21
19
|
|
|
@@ -43,6 +41,9 @@ class StreamManager:
|
|
|
43
41
|
video=self.video_query,
|
|
44
42
|
audio=self.audio_query,
|
|
45
43
|
event=self.event_query,
|
|
44
|
+
axis_orig_sw="&Axis-Orig-Sw=true"
|
|
45
|
+
if self.device.config.is_companion
|
|
46
|
+
else "",
|
|
46
47
|
)
|
|
47
48
|
_LOGGER.debug(rtsp_url)
|
|
48
49
|
return rtsp_url
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: axis
|
|
3
|
+
Version: 65
|
|
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.12
|
|
16
|
+
Classifier: Topic :: Home Automation
|
|
17
|
+
Requires-Python: >=3.12.0
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: faust-cchardet>=2.1.18
|
|
21
|
+
Requires-Dist: httpx>=0.26
|
|
22
|
+
Requires-Dist: orjson>3.9
|
|
23
|
+
Requires-Dist: packaging>23
|
|
24
|
+
Requires-Dist: xmltodict>=0.13.0
|
|
25
|
+
Provides-Extra: requirements
|
|
26
|
+
Requires-Dist: httpx==0.28.1; extra == "requirements"
|
|
27
|
+
Requires-Dist: orjson==3.11.1; extra == "requirements"
|
|
28
|
+
Requires-Dist: packaging==25.0; extra == "requirements"
|
|
29
|
+
Requires-Dist: xmltodict==0.14.2; extra == "requirements"
|
|
30
|
+
Provides-Extra: requirements-test
|
|
31
|
+
Requires-Dist: mypy==1.17.1; extra == "requirements-test"
|
|
32
|
+
Requires-Dist: pytest==8.4.1; extra == "requirements-test"
|
|
33
|
+
Requires-Dist: pytest-aiohttp==1.1.0; extra == "requirements-test"
|
|
34
|
+
Requires-Dist: pytest-asyncio==0.26.0; extra == "requirements-test"
|
|
35
|
+
Requires-Dist: pytest-cov==6.2.1; extra == "requirements-test"
|
|
36
|
+
Requires-Dist: respx==0.22.0; extra == "requirements-test"
|
|
37
|
+
Requires-Dist: ruff==0.11.11; extra == "requirements-test"
|
|
38
|
+
Requires-Dist: types-xmltodict==v0.14.0.20241009; extra == "requirements-test"
|
|
39
|
+
Provides-Extra: requirements-dev
|
|
40
|
+
Requires-Dist: pre-commit==4.2.0; extra == "requirements-dev"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
Python project to set up a connection towards Axis Communications devices and to subscribe to specific events on the metadatastream.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
faust-cchardet>=2.1.18
|
|
2
|
+
httpx>=0.26
|
|
3
|
+
orjson>3.9
|
|
4
|
+
packaging>23
|
|
5
|
+
xmltodict>=0.13.0
|
|
6
|
+
|
|
7
|
+
[requirements]
|
|
8
|
+
httpx==0.28.1
|
|
9
|
+
orjson==3.11.1
|
|
10
|
+
packaging==25.0
|
|
11
|
+
xmltodict==0.14.2
|
|
12
|
+
|
|
13
|
+
[requirements-dev]
|
|
14
|
+
pre-commit==4.2.0
|
|
15
|
+
|
|
16
|
+
[requirements-test]
|
|
17
|
+
mypy==1.17.1
|
|
18
|
+
pytest==8.4.1
|
|
19
|
+
pytest-aiohttp==1.1.0
|
|
20
|
+
pytest-asyncio==0.26.0
|
|
21
|
+
pytest-cov==6.2.1
|
|
22
|
+
respx==0.22.0
|
|
23
|
+
ruff==0.11.11
|
|
24
|
+
types-xmltodict==v0.14.0.20241009
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools==
|
|
2
|
+
requires = ["setuptools==80.9.0", "wheel==0.46.1"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "axis"
|
|
7
|
-
version = "
|
|
7
|
+
version = "65"
|
|
8
8
|
license = {text = "MIT"}
|
|
9
9
|
description = "A Python library for communicating with devices from Axis Communications"
|
|
10
10
|
readme = "README.md"
|
|
@@ -20,6 +20,7 @@ classifiers = [
|
|
|
20
20
|
]
|
|
21
21
|
requires-python = ">=3.12.0"
|
|
22
22
|
dependencies = [
|
|
23
|
+
"faust-cchardet>=2.1.18",
|
|
23
24
|
"httpx>=0.26",
|
|
24
25
|
"orjson>3.9",
|
|
25
26
|
"packaging>23",
|
|
@@ -28,24 +29,23 @@ dependencies = [
|
|
|
28
29
|
|
|
29
30
|
[project.optional-dependencies]
|
|
30
31
|
requirements = [
|
|
31
|
-
"httpx==0.
|
|
32
|
-
"orjson==3.
|
|
33
|
-
"packaging==
|
|
32
|
+
"httpx==0.28.1",
|
|
33
|
+
"orjson==3.11.1",
|
|
34
|
+
"packaging==25.0",
|
|
34
35
|
"xmltodict==0.14.2",
|
|
35
36
|
]
|
|
36
|
-
|
|
37
|
-
"mypy==1.
|
|
38
|
-
"pytest==8.
|
|
39
|
-
"pytest-aiohttp==1.0
|
|
40
|
-
"pytest-asyncio==0.
|
|
41
|
-
"pytest-cov==
|
|
42
|
-
"respx==0.
|
|
43
|
-
"ruff==0.
|
|
44
|
-
"types-orjson==3.6.2",
|
|
37
|
+
requirements-test = [
|
|
38
|
+
"mypy==1.17.1",
|
|
39
|
+
"pytest==8.4.1",
|
|
40
|
+
"pytest-aiohttp==1.1.0",
|
|
41
|
+
"pytest-asyncio==0.26.0",
|
|
42
|
+
"pytest-cov==6.2.1",
|
|
43
|
+
"respx==0.22.0",
|
|
44
|
+
"ruff==0.11.11",
|
|
45
45
|
"types-xmltodict==v0.14.0.20241009",
|
|
46
46
|
]
|
|
47
|
-
|
|
48
|
-
"pre-commit==4.0
|
|
47
|
+
requirements-dev = [
|
|
48
|
+
"pre-commit==4.2.0"
|
|
49
49
|
]
|
|
50
50
|
|
|
51
51
|
[project.urls]
|
|
@@ -148,8 +148,6 @@ lint.select = [
|
|
|
148
148
|
]
|
|
149
149
|
|
|
150
150
|
lint.ignore = [
|
|
151
|
-
"ANN101", # Missing type annotation for {name} in method
|
|
152
|
-
"ANN102", # Missing type annotation for {name} in classmethod
|
|
153
151
|
"ANN401", # Dynamically typed expressions (typing.Any) are disallowed in {name}
|
|
154
152
|
"COM812", # Trailing comma missing
|
|
155
153
|
"D203", # 1 blank line required before class docstring
|
|
@@ -28,6 +28,7 @@ def test_configuration():
|
|
|
28
28
|
assert config.web_proto == "https"
|
|
29
29
|
assert config.verify_ssl is True
|
|
30
30
|
assert config.url == "https://192.168.0.1:443"
|
|
31
|
+
assert config.is_companion is False
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
def test_minimal_configuration():
|
|
@@ -47,3 +48,4 @@ def test_minimal_configuration():
|
|
|
47
48
|
assert config.web_proto == "http"
|
|
48
49
|
assert config.verify_ssl is False
|
|
49
50
|
assert config.url == "http://192.168.1.1:80"
|
|
51
|
+
assert config.is_companion is False
|
|
@@ -20,7 +20,7 @@ def ports(axis_device) -> Ports:
|
|
|
20
20
|
async def test_ports(respx_mock, ports: Ports) -> None:
|
|
21
21
|
"""Test that different types of ports work."""
|
|
22
22
|
update_ports_route = respx_mock.post(f"http://{HOST}/axis-cgi/param.cgi").respond(
|
|
23
|
-
|
|
23
|
+
content="""root.Input.NbrOfInputs=3
|
|
24
24
|
root.IOPort.I0.Direction=input
|
|
25
25
|
root.IOPort.I0.Usage=Button
|
|
26
26
|
root.IOPort.I1.Configurable=no
|
|
@@ -44,8 +44,8 @@ root.IOPort.I3.Output.Mode=bistable
|
|
|
44
44
|
root.IOPort.I3.Output.Name=Tampering
|
|
45
45
|
root.IOPort.I3.Output.PulseTime=0
|
|
46
46
|
root.Output.NbrOfOutputs=1
|
|
47
|
-
""",
|
|
48
|
-
headers={"Content-Type": "text/plain"},
|
|
47
|
+
""".encode("iso-8859-1"),
|
|
48
|
+
headers={"Content-Type": "text/plain; charset=iso-8859-1"},
|
|
49
49
|
)
|
|
50
50
|
|
|
51
51
|
await ports.update()
|
|
@@ -99,8 +99,8 @@ root.Output.NbrOfOutputs=1
|
|
|
99
99
|
async def test_no_ports(respx_mock, ports: Ports) -> None:
|
|
100
100
|
"""Test that no ports also work."""
|
|
101
101
|
route = respx_mock.post(f"http://{HOST}/axis-cgi/param.cgi").respond(
|
|
102
|
-
|
|
103
|
-
headers={"Content-Type": "text/plain"},
|
|
102
|
+
content="".encode("iso-8859-1"),
|
|
103
|
+
headers={"Content-Type": "text/plain; charset=iso-8859-1"},
|
|
104
104
|
)
|
|
105
105
|
|
|
106
106
|
await ports.update()
|
|
@@ -20,6 +20,12 @@ def stream_manager(axis_device) -> StreamManager:
|
|
|
20
20
|
return axis_device.stream
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def stream_manager_companion(axis_companion_device) -> StreamManager:
|
|
25
|
+
"""Return the StreamManager mock object."""
|
|
26
|
+
return axis_companion_device.stream
|
|
27
|
+
|
|
28
|
+
|
|
23
29
|
async def test_stream_url(stream_manager):
|
|
24
30
|
"""Verify stream url."""
|
|
25
31
|
assert stream_manager.video_query == 0
|
|
@@ -38,6 +44,24 @@ async def test_stream_url(stream_manager):
|
|
|
38
44
|
)
|
|
39
45
|
|
|
40
46
|
|
|
47
|
+
async def test_stream_url_companion(stream_manager_companion):
|
|
48
|
+
"""Verify stream url."""
|
|
49
|
+
assert stream_manager_companion.video_query == 0
|
|
50
|
+
assert stream_manager_companion.audio_query == 0
|
|
51
|
+
assert stream_manager_companion.event_query == "off"
|
|
52
|
+
assert (
|
|
53
|
+
stream_manager_companion.stream_url
|
|
54
|
+
== f"rtsp://{HOST}/axis-media/media.amp?video=0&audio=0&event=off&Axis-Orig-Sw=true"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
stream_manager_companion.event = True
|
|
58
|
+
assert stream_manager_companion.event_query == "on"
|
|
59
|
+
assert (
|
|
60
|
+
stream_manager_companion.stream_url
|
|
61
|
+
== f"rtsp://{HOST}/axis-media/media.amp?video=0&audio=0&event=on&Axis-Orig-Sw=true"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
41
65
|
@patch("axis.stream_manager.RTSPClient")
|
|
42
66
|
async def test_initialize_stream(rtsp_client, stream_manager):
|
|
43
67
|
"""Test stream commands."""
|
|
@@ -49,6 +49,12 @@ def vapix(axis_device: AxisDevice) -> Vapix:
|
|
|
49
49
|
return axis_device.vapix
|
|
50
50
|
|
|
51
51
|
|
|
52
|
+
@pytest.fixture
|
|
53
|
+
def vapix_companion_device(axis_companion_device: AxisDevice) -> Vapix:
|
|
54
|
+
"""Return the vapix object."""
|
|
55
|
+
return axis_companion_device.vapix
|
|
56
|
+
|
|
57
|
+
|
|
52
58
|
def test_vapix_not_initialized(vapix: Vapix) -> None:
|
|
53
59
|
"""Test Vapix class without initialising any data."""
|
|
54
60
|
assert dict(vapix.basic_device_info.items()) == {}
|
|
@@ -92,8 +98,10 @@ async def test_initialize(respx_mock, vapix: Vapix):
|
|
|
92
98
|
}
|
|
93
99
|
)
|
|
94
100
|
|
|
95
|
-
respx_mock.post("/axis-cgi/param.cgi").respond(
|
|
96
|
-
|
|
101
|
+
respx_mock.post("/axis-cgi/param.cgi").respond(
|
|
102
|
+
content=PARAM_CGI_RESPONSE.encode("iso-8859-1"),
|
|
103
|
+
headers={"Content-Type": "text/plain; charset=iso-8859-1"},
|
|
104
|
+
)
|
|
97
105
|
respx_mock.post("/axis-cgi/applications/list.cgi").respond(
|
|
98
106
|
text=APPLICATIONS_RESPONSE,
|
|
99
107
|
headers={"Content-Type": "text/xml"},
|
|
@@ -219,14 +227,15 @@ async def test_initialize_api_discovery_unsupported(respx_mock, vapix: Vapix):
|
|
|
219
227
|
async def test_initialize_param_cgi(respx_mock, vapix: Vapix):
|
|
220
228
|
"""Verify that you can list parameters."""
|
|
221
229
|
respx_mock.post("/axis-cgi/param.cgi").respond(
|
|
222
|
-
|
|
223
|
-
headers={"Content-Type": "text/plain"},
|
|
230
|
+
content=PARAM_CGI_RESPONSE.encode("iso-8859-1"),
|
|
231
|
+
headers={"Content-Type": "text/plain; charset=iso-8859-1"},
|
|
224
232
|
)
|
|
225
233
|
respx_mock.post("/axis-cgi/lightcontrol.cgi").respond(
|
|
226
234
|
json=LIGHT_CONTROL_RESPONSE,
|
|
227
235
|
)
|
|
228
236
|
await vapix.initialize_param_cgi()
|
|
229
237
|
|
|
238
|
+
assert "Axis-Orig-Sw" not in respx_mock.calls.last.request.url.params
|
|
230
239
|
assert vapix.firmware_version == "9.10.1"
|
|
231
240
|
assert vapix.product_number == "M1065-LW"
|
|
232
241
|
assert vapix.product_type == "Network Camera"
|
|
@@ -243,11 +252,43 @@ async def test_initialize_param_cgi(respx_mock, vapix: Vapix):
|
|
|
243
252
|
assert vapix.users.supported
|
|
244
253
|
|
|
245
254
|
|
|
255
|
+
async def test_initialize_param_cgi_for_companion_device(
|
|
256
|
+
respx_mock, vapix_companion_device: Vapix
|
|
257
|
+
):
|
|
258
|
+
"""Verify that you can list parameters."""
|
|
259
|
+
respx_mock.post("/axis-cgi/param.cgi").respond(
|
|
260
|
+
content=PARAM_CGI_RESPONSE.encode("iso-8859-1"),
|
|
261
|
+
headers={"Content-Type": "text/plain; charset=iso-8859-1"},
|
|
262
|
+
)
|
|
263
|
+
respx_mock.post("/axis-cgi/lightcontrol.cgi").respond(
|
|
264
|
+
json=LIGHT_CONTROL_RESPONSE,
|
|
265
|
+
)
|
|
266
|
+
await vapix_companion_device.initialize_param_cgi()
|
|
267
|
+
|
|
268
|
+
assert "Axis-Orig-Sw" in respx_mock.calls.last.request.url.params
|
|
269
|
+
|
|
270
|
+
assert vapix_companion_device.firmware_version == "9.10.1"
|
|
271
|
+
assert vapix_companion_device.product_number == "M1065-LW"
|
|
272
|
+
assert vapix_companion_device.product_type == "Network Camera"
|
|
273
|
+
assert vapix_companion_device.serial_number == "ACCC12345678"
|
|
274
|
+
assert len(vapix_companion_device.streaming_profiles) == 2
|
|
275
|
+
|
|
276
|
+
assert len(vapix_companion_device.basic_device_info) == 0
|
|
277
|
+
assert len(vapix_companion_device.ports.values()) == 1
|
|
278
|
+
assert len(vapix_companion_device.light_control.values()) == 1
|
|
279
|
+
assert len(vapix_companion_device.mqtt) == 0
|
|
280
|
+
assert len(vapix_companion_device.stream_profiles) == 0
|
|
281
|
+
assert len(vapix_companion_device.params.stream_profile_handler) == 1
|
|
282
|
+
|
|
283
|
+
assert vapix_companion_device.users.supported
|
|
284
|
+
|
|
285
|
+
|
|
246
286
|
async def test_initialize_params_no_data(respx_mock, vapix: Vapix):
|
|
247
287
|
"""Verify that you can list parameters."""
|
|
248
|
-
param_route = respx_mock.post(
|
|
249
|
-
"
|
|
250
|
-
|
|
288
|
+
param_route = respx_mock.post("/axis-cgi/param.cgi").respond(
|
|
289
|
+
content="".encode("iso-8859-1"),
|
|
290
|
+
headers={"Content-Type": "text/plain; charset=iso-8859-1"},
|
|
291
|
+
)
|
|
251
292
|
await vapix.initialize_param_cgi(preload_data=False)
|
|
252
293
|
|
|
253
294
|
assert param_route.call_count == 4
|
|
@@ -256,8 +297,8 @@ async def test_initialize_params_no_data(respx_mock, vapix: Vapix):
|
|
|
256
297
|
async def test_initialize_applications(respx_mock, vapix: Vapix):
|
|
257
298
|
"""Verify you can list and retrieve descriptions of applications."""
|
|
258
299
|
respx_mock.post("/axis-cgi/param.cgi").respond(
|
|
259
|
-
|
|
260
|
-
headers={"Content-Type": "text/plain"},
|
|
300
|
+
content=PARAM_CGI_RESPONSE.encode("iso-8859-1"),
|
|
301
|
+
headers={"Content-Type": "text/plain; charset=iso-8859-1"},
|
|
261
302
|
)
|
|
262
303
|
respx_mock.post("/axis-cgi/lightcontrol.cgi").respond(
|
|
263
304
|
json=LIGHT_CONTROL_RESPONSE,
|
|
@@ -296,8 +337,8 @@ async def test_initialize_applications(respx_mock, vapix: Vapix):
|
|
|
296
337
|
async def test_initialize_applications_unauthorized(respx_mock, vapix: Vapix, code):
|
|
297
338
|
"""Verify initialize applications doesnt break on too low credentials."""
|
|
298
339
|
respx_mock.post("/axis-cgi/param.cgi").respond(
|
|
299
|
-
|
|
300
|
-
headers={"Content-Type": "text/plain"},
|
|
340
|
+
content=PARAM_CGI_RESPONSE.encode("iso-8859-1"),
|
|
341
|
+
headers={"Content-Type": "text/plain; charset=iso-8859-1"},
|
|
301
342
|
)
|
|
302
343
|
respx_mock.post("/axis-cgi/lightcontrol.cgi").respond(
|
|
303
344
|
json=LIGHT_CONTROL_RESPONSE,
|
|
@@ -313,8 +354,8 @@ async def test_initialize_applications_unauthorized(respx_mock, vapix: Vapix, co
|
|
|
313
354
|
async def test_initialize_applications_not_running(respx_mock, vapix: Vapix):
|
|
314
355
|
"""Verify you can list and retrieve descriptions of applications."""
|
|
315
356
|
respx_mock.post("/axis-cgi/param.cgi").respond(
|
|
316
|
-
|
|
317
|
-
headers={"Content-Type": "text/plain"},
|
|
357
|
+
content=PARAM_CGI_RESPONSE.encode("iso-8859-1"),
|
|
358
|
+
headers={"Content-Type": "text/plain; charset=iso-8859-1"},
|
|
318
359
|
)
|
|
319
360
|
respx_mock.post("/axis-cgi/lightcontrol.cgi").respond(
|
|
320
361
|
json=LIGHT_CONTROL_RESPONSE,
|
|
@@ -351,10 +392,10 @@ async def test_initialize_event_instances(respx_mock, vapix: Vapix):
|
|
|
351
392
|
|
|
352
393
|
async def test_applications_dont_load_without_params(respx_mock, vapix: Vapix):
|
|
353
394
|
"""Applications depends on param cgi to be loaded first."""
|
|
354
|
-
param_route = respx_mock.post(
|
|
355
|
-
"
|
|
356
|
-
|
|
357
|
-
|
|
395
|
+
param_route = respx_mock.post("/axis-cgi/param.cgi").respond(
|
|
396
|
+
content="key=value".encode("iso-8859-1"),
|
|
397
|
+
headers={"Content-Type": "text/plain; charset=iso-8859-1"},
|
|
398
|
+
)
|
|
358
399
|
applications_route = respx_mock.post("/axis-cgi/applications/list.cgi")
|
|
359
400
|
|
|
360
401
|
await vapix.initialize_param_cgi(preload_data=False)
|
axis-63/PKG-INFO
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: axis
|
|
3
|
-
Version: 63
|
|
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.12
|
|
16
|
-
Classifier: Topic :: Home Automation
|
|
17
|
-
Requires-Python: >=3.12.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.
|
axis-63/axis.egg-info/PKG-INFO
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: axis
|
|
3
|
-
Version: 63
|
|
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.12
|
|
16
|
-
Classifier: Topic :: Home Automation
|
|
17
|
-
Requires-Python: >=3.12.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.
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
httpx>=0.26
|
|
2
|
-
orjson>3.9
|
|
3
|
-
packaging>23
|
|
4
|
-
xmltodict>=0.13.0
|
|
5
|
-
|
|
6
|
-
[requirements]
|
|
7
|
-
httpx==0.27.2
|
|
8
|
-
orjson==3.10.9
|
|
9
|
-
packaging==24.1
|
|
10
|
-
xmltodict==0.14.2
|
|
11
|
-
|
|
12
|
-
[requirements_dev]
|
|
13
|
-
pre-commit==4.0.1
|
|
14
|
-
|
|
15
|
-
[requirements_test]
|
|
16
|
-
mypy==1.12.1
|
|
17
|
-
pytest==8.3.3
|
|
18
|
-
pytest-aiohttp==1.0.5
|
|
19
|
-
pytest-asyncio==0.24.0
|
|
20
|
-
pytest-cov==5.0.0
|
|
21
|
-
respx==0.21.1
|
|
22
|
-
ruff==0.7.0
|
|
23
|
-
types-orjson==3.6.2
|
|
24
|
-
types-xmltodict==v0.14.0.20241009
|
{axis-63 → axis-65}/LICENSE
RENAMED
|
File without changes
|
{axis-63 → axis-65}/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{axis-63 → axis-65}/axis/rtsp.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{axis-63 → axis-65}/setup.cfg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|