axis 73__tar.gz → 75__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-73 → axis-75}/PKG-INFO +27 -19
- {axis-73 → axis-75}/README.md +15 -7
- {axis-73 → axis-75}/axis/cli/core/router.py +10 -3
- {axis-73 → axis-75}/axis/cli/main.py +5 -1
- {axis-73 → axis-75}/axis/cli/packs/events.py +44 -1
- axis-75/axis/interfaces/firmware_management.py +21 -0
- axis-75/axis/interfaces/restart_cgi.py +17 -0
- {axis-73 → axis-75}/axis/interfaces/vapix.py +30 -15
- axis-75/axis/interfaces/virtual_input.py +38 -0
- {axis-73 → axis-75}/axis/models/api.py +2 -0
- {axis-73 → axis-75}/axis/models/api_discovery.py +2 -0
- {axis-73 → axis-75}/axis/models/basic_device_info.py +2 -0
- axis-75/axis/models/firmware_management.py +64 -0
- {axis-73 → axis-75}/axis/models/parameters/properties.py +11 -0
- axis-75/axis/models/restart_cgi.py +15 -0
- axis-75/axis/models/virtual_input.py +138 -0
- {axis-73 → axis-75}/axis/rtsp.py +115 -19
- {axis-73 → axis-75}/axis/stream_manager.py +4 -2
- {axis-73 → axis-75}/axis/websocket.py +22 -5
- {axis-73 → axis-75}/axis.egg-info/PKG-INFO +27 -19
- {axis-73 → axis-75}/axis.egg-info/SOURCES.txt +9 -1
- {axis-73 → axis-75}/axis.egg-info/requires.txt +11 -11
- {axis-73 → axis-75}/pyproject.toml +17 -17
- {axis-73 → axis-75}/tests/test_auth_scheme.py +5 -4
- {axis-73 → axis-75}/tests/test_configuration.py +1 -0
- {axis-73 → axis-75}/tests/test_event.py +13 -0
- axis-75/tests/test_firmware_management.py +116 -0
- axis-75/tests/test_http_client_compat.py +219 -0
- axis-75/tests/test_main_http_client.py +263 -0
- {axis-73 → axis-75}/tests/test_request_response_contracts.py +11 -0
- axis-75/tests/test_restart_cgi.py +34 -0
- {axis-73 → axis-75}/tests/test_rtsp.py +235 -11
- {axis-73 → axis-75}/tests/test_stream_manager.py +54 -0
- {axis-73 → axis-75}/tests/test_vapix.py +109 -0
- axis-75/tests/test_virtual_input.py +168 -0
- {axis-73 → axis-75}/tests/test_websocket.py +57 -5
- axis-73/axis/interfaces/aiohttp_digest.py +0 -264
- axis-73/tests/test_http_client_compat.py +0 -370
- axis-73/tests/test_main_http_client.py +0 -39
- {axis-73 → axis-75}/LICENSE +0 -0
- {axis-73 → axis-75}/axis/__init__.py +0 -0
- {axis-73 → axis-75}/axis/__main__.py +0 -0
- {axis-73 → axis-75}/axis/cli/__init__.py +0 -0
- {axis-73 → axis-75}/axis/cli/core/__init__.py +0 -0
- {axis-73 → axis-75}/axis/cli/core/context.py +0 -0
- {axis-73 → axis-75}/axis/cli/core/contracts.py +0 -0
- {axis-73 → axis-75}/axis/cli/core/gateway.py +0 -0
- {axis-73 → axis-75}/axis/cli/core/io.py +0 -0
- {axis-73 → axis-75}/axis/cli/core/registry.py +0 -0
- {axis-73 → axis-75}/axis/cli/packs/__init__.py +0 -0
- {axis-73 → axis-75}/axis/cli/packs/accounts.py +0 -0
- {axis-73 → axis-75}/axis/cli/packs/api.py +0 -0
- {axis-73 → axis-75}/axis/cli/packs/devices.py +0 -0
- {axis-73 → axis-75}/axis/cli/packs/navigation.py +0 -0
- {axis-73 → axis-75}/axis/device.py +0 -0
- {axis-73 → axis-75}/axis/errors.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/__init__.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/api_discovery.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/api_handler.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/applications/__init__.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/applications/application_handler.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/applications/applications.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/applications/fence_guard.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/applications/loitering_guard.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/applications/motion_guard.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/applications/object_analytics.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/applications/vmd4.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/basic_device_info.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/event_instances.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/event_manager.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/light_control.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/mqtt.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/parameters/__init__.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/parameters/brand.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/parameters/image.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/parameters/io_port.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/parameters/param_cgi.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/parameters/param_handler.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/parameters/properties.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/parameters/ptz.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/parameters/stream_profile.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/pir_sensor_configuration.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/port_cgi.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/port_management.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/ptz.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/pwdgrp_cgi.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/stream_profiles.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/temperature_control.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/user_groups.py +0 -0
- {axis-73 → axis-75}/axis/interfaces/view_areas.py +0 -0
- {axis-73 → axis-75}/axis/models/__init__.py +0 -0
- {axis-73 → axis-75}/axis/models/applications/__init__.py +0 -0
- {axis-73 → axis-75}/axis/models/applications/application.py +0 -0
- {axis-73 → axis-75}/axis/models/applications/fence_guard.py +0 -0
- {axis-73 → axis-75}/axis/models/applications/loitering_guard.py +0 -0
- {axis-73 → axis-75}/axis/models/applications/motion_guard.py +0 -0
- {axis-73 → axis-75}/axis/models/applications/object_analytics.py +0 -0
- {axis-73 → axis-75}/axis/models/applications/vmd4.py +0 -0
- {axis-73 → axis-75}/axis/models/configuration.py +0 -0
- {axis-73 → axis-75}/axis/models/event.py +0 -0
- {axis-73 → axis-75}/axis/models/event_instance.py +0 -0
- {axis-73 → axis-75}/axis/models/light_control.py +0 -0
- {axis-73 → axis-75}/axis/models/mqtt.py +0 -0
- {axis-73 → axis-75}/axis/models/parameters/__init__.py +0 -0
- {axis-73 → axis-75}/axis/models/parameters/brand.py +0 -0
- {axis-73 → axis-75}/axis/models/parameters/image.py +0 -0
- {axis-73 → axis-75}/axis/models/parameters/io_port.py +0 -0
- {axis-73 → axis-75}/axis/models/parameters/param_cgi.py +0 -0
- {axis-73 → axis-75}/axis/models/parameters/ptz.py +0 -0
- {axis-73 → axis-75}/axis/models/parameters/stream_profile.py +0 -0
- {axis-73 → axis-75}/axis/models/pir_sensor_configuration.py +0 -0
- {axis-73 → axis-75}/axis/models/port_cgi.py +0 -0
- {axis-73 → axis-75}/axis/models/port_management.py +0 -0
- {axis-73 → axis-75}/axis/models/ptz_cgi.py +0 -0
- {axis-73 → axis-75}/axis/models/pwdgrp_cgi.py +0 -0
- {axis-73 → axis-75}/axis/models/stream_profile.py +0 -0
- {axis-73 → axis-75}/axis/models/temperature_control.py +0 -0
- {axis-73 → axis-75}/axis/models/user_group.py +0 -0
- {axis-73 → axis-75}/axis/models/view_area.py +0 -0
- {axis-73 → axis-75}/axis/py.typed +0 -0
- {axis-73 → axis-75}/axis/stream_transport.py +0 -0
- {axis-73 → axis-75}/axis.egg-info/dependency_links.txt +0 -0
- {axis-73 → axis-75}/axis.egg-info/entry_points.txt +0 -0
- {axis-73 → axis-75}/axis.egg-info/top_level.txt +0 -0
- {axis-73 → axis-75}/setup.cfg +0 -0
- {axis-73 → axis-75}/tests/test_api_discovery.py +0 -0
- {axis-73 → axis-75}/tests/test_api_handler.py +0 -0
- {axis-73 → axis-75}/tests/test_basic_device_info.py +0 -0
- {axis-73 → axis-75}/tests/test_conftest.py +0 -0
- {axis-73 → axis-75}/tests/test_device.py +0 -0
- {axis-73 → axis-75}/tests/test_event_instances.py +0 -0
- {axis-73 → axis-75}/tests/test_event_instances_protocol_parity.py +0 -0
- {axis-73 → axis-75}/tests/test_event_stream.py +0 -0
- {axis-73 → axis-75}/tests/test_light_control.py +0 -0
- {axis-73 → axis-75}/tests/test_mqtt.py +0 -0
- {axis-73 → axis-75}/tests/test_pir_sensor_configuration.py +0 -0
- {axis-73 → axis-75}/tests/test_port_cgi.py +0 -0
- {axis-73 → axis-75}/tests/test_port_management.py +0 -0
- {axis-73 → axis-75}/tests/test_ptz.py +0 -0
- {axis-73 → axis-75}/tests/test_pwdgrp_cgi.py +0 -0
- {axis-73 → axis-75}/tests/test_stream_profiles.py +0 -0
- {axis-73 → axis-75}/tests/test_temperature_control.py +0 -0
- {axis-73 → axis-75}/tests/test_user_groups.py +0 -0
- {axis-73 → axis-75}/tests/test_view_areas.py +0 -0
{axis-73 → axis-75}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: axis
|
|
3
|
-
Version:
|
|
3
|
+
Version: 75
|
|
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
|
|
@@ -17,30 +17,30 @@ Classifier: Topic :: Home Automation
|
|
|
17
17
|
Requires-Python: >=3.14.0
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
License-File: LICENSE
|
|
20
|
-
Requires-Dist: aiohttp>=3.
|
|
20
|
+
Requires-Dist: aiohttp>=3.14.0
|
|
21
21
|
Requires-Dist: faust-cchardet>=2.1.18
|
|
22
22
|
Requires-Dist: orjson>3.9
|
|
23
23
|
Requires-Dist: packaging>23
|
|
24
24
|
Requires-Dist: tomli>=2.0
|
|
25
25
|
Requires-Dist: tomli-w>=1.0
|
|
26
26
|
Requires-Dist: xmltodict>=0.13.0
|
|
27
|
-
Requires-Dist: zeroconf
|
|
27
|
+
Requires-Dist: zeroconf>=0.150.0
|
|
28
28
|
Provides-Extra: requirements
|
|
29
|
-
Requires-Dist: aiohttp==3.14.
|
|
30
|
-
Requires-Dist: orjson==3.
|
|
31
|
-
Requires-Dist: packaging==26.
|
|
29
|
+
Requires-Dist: aiohttp==3.14.5; extra == "requirements"
|
|
30
|
+
Requires-Dist: orjson==3.13.0; extra == "requirements"
|
|
31
|
+
Requires-Dist: packaging==26.3; extra == "requirements"
|
|
32
32
|
Requires-Dist: xmltodict==1.0.4; extra == "requirements"
|
|
33
|
-
Requires-Dist: zeroconf
|
|
33
|
+
Requires-Dist: zeroconf>=0.150.0; extra == "requirements"
|
|
34
34
|
Provides-Extra: requirements-test
|
|
35
|
-
Requires-Dist: mypy==2.1
|
|
36
|
-
Requires-Dist: pytest==9.
|
|
35
|
+
Requires-Dist: mypy==2.4.1; extra == "requirements-test"
|
|
36
|
+
Requires-Dist: pytest==9.2.0; extra == "requirements-test"
|
|
37
37
|
Requires-Dist: pytest-aiohttp==1.1.1; extra == "requirements-test"
|
|
38
|
-
Requires-Dist: pytest-asyncio==1.4.
|
|
38
|
+
Requires-Dist: pytest-asyncio==1.4.1.post1; extra == "requirements-test"
|
|
39
39
|
Requires-Dist: pytest-cov==7.1.0; extra == "requirements-test"
|
|
40
|
-
Requires-Dist: ruff==0.
|
|
40
|
+
Requires-Dist: ruff==0.17.4; extra == "requirements-test"
|
|
41
41
|
Requires-Dist: types-xmltodict==v1.0.1.20260518; extra == "requirements-test"
|
|
42
42
|
Provides-Extra: requirements-dev
|
|
43
|
-
Requires-Dist: pre-commit==4.
|
|
43
|
+
Requires-Dist: pre-commit==4.7.0; extra == "requirements-dev"
|
|
44
44
|
Dynamic: license-file
|
|
45
45
|
|
|
46
46
|
# axis
|
|
@@ -62,11 +62,11 @@ Or run the bootstrap script, which installs `uv` if needed and provisions Python
|
|
|
62
62
|
./setup.sh
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
Dependencies are locked via `uv.lock`.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
Dependencies are locked via `uv.lock`. The pre-commit hook installed by `./setup.sh`
|
|
66
|
+
automatically refreshes it when `pyproject.toml` or `uv.lock` is included in a commit.
|
|
67
|
+
If the hook modifies it, include the updated lockfile when retrying the commit.
|
|
68
|
+
CI rejects stale or missing lockfiles, even when local hooks are bypassed, and
|
|
69
|
+
runs checks with `--locked` to prevent implicit lockfile updates.
|
|
70
70
|
|
|
71
71
|
Run checks with `uv`:
|
|
72
72
|
|
|
@@ -77,6 +77,14 @@ uv run mypy axis
|
|
|
77
77
|
uv run pytest
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
+
Pull requests expose independent **Lint**, **Formatting**, **Typing**, and
|
|
81
|
+
**Tests (Python 3.14)** checks in GitHub. A failed check does not prevent the
|
|
82
|
+
others from running. Ruff lint findings also appear as file-and-line annotations.
|
|
83
|
+
The test job uploads JUnit XML and XML/HTML coverage reports as the
|
|
84
|
+
`test-reports-python-3.14` artifact, including when tests fail.
|
|
85
|
+
The workflow run summary shows the test outcome, counts, duration, line coverage,
|
|
86
|
+
and configured coverage threshold. Missing reports are flagged explicitly.
|
|
87
|
+
|
|
80
88
|
Initial `ty` support is configured as an opt-in check and does not replace `mypy`:
|
|
81
89
|
|
|
82
90
|
```bash
|
|
@@ -123,12 +131,12 @@ Examples:
|
|
|
123
131
|
```python
|
|
124
132
|
@dataclass
|
|
125
133
|
class ListApisRequest(ApiRequest[GetAllApisResponse]):
|
|
126
|
-
|
|
134
|
+
response_type = GetAllApisResponse
|
|
127
135
|
|
|
128
136
|
|
|
129
137
|
@dataclass
|
|
130
138
|
class SetPortsRequest(ApiRequest[ApiResponse[bytes]]):
|
|
131
|
-
|
|
139
|
+
response_type = BytesResponse
|
|
132
140
|
```
|
|
133
141
|
|
|
134
142
|
Handler methods may unwrap `.data` when they intentionally preserve a bytes-returning boundary.
|
{axis-73 → axis-75}/README.md
RENAMED
|
@@ -17,11 +17,11 @@ Or run the bootstrap script, which installs `uv` if needed and provisions Python
|
|
|
17
17
|
./setup.sh
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
Dependencies are locked via `uv.lock`.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
Dependencies are locked via `uv.lock`. The pre-commit hook installed by `./setup.sh`
|
|
21
|
+
automatically refreshes it when `pyproject.toml` or `uv.lock` is included in a commit.
|
|
22
|
+
If the hook modifies it, include the updated lockfile when retrying the commit.
|
|
23
|
+
CI rejects stale or missing lockfiles, even when local hooks are bypassed, and
|
|
24
|
+
runs checks with `--locked` to prevent implicit lockfile updates.
|
|
25
25
|
|
|
26
26
|
Run checks with `uv`:
|
|
27
27
|
|
|
@@ -32,6 +32,14 @@ uv run mypy axis
|
|
|
32
32
|
uv run pytest
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
Pull requests expose independent **Lint**, **Formatting**, **Typing**, and
|
|
36
|
+
**Tests (Python 3.14)** checks in GitHub. A failed check does not prevent the
|
|
37
|
+
others from running. Ruff lint findings also appear as file-and-line annotations.
|
|
38
|
+
The test job uploads JUnit XML and XML/HTML coverage reports as the
|
|
39
|
+
`test-reports-python-3.14` artifact, including when tests fail.
|
|
40
|
+
The workflow run summary shows the test outcome, counts, duration, line coverage,
|
|
41
|
+
and configured coverage threshold. Missing reports are flagged explicitly.
|
|
42
|
+
|
|
35
43
|
Initial `ty` support is configured as an opt-in check and does not replace `mypy`:
|
|
36
44
|
|
|
37
45
|
```bash
|
|
@@ -78,12 +86,12 @@ Examples:
|
|
|
78
86
|
```python
|
|
79
87
|
@dataclass
|
|
80
88
|
class ListApisRequest(ApiRequest[GetAllApisResponse]):
|
|
81
|
-
|
|
89
|
+
response_type = GetAllApisResponse
|
|
82
90
|
|
|
83
91
|
|
|
84
92
|
@dataclass
|
|
85
93
|
class SetPortsRequest(ApiRequest[ApiResponse[bytes]]):
|
|
86
|
-
|
|
94
|
+
response_type = BytesResponse
|
|
87
95
|
```
|
|
88
96
|
|
|
89
97
|
Handler methods may unwrap `.data` when they intentionally preserve a bytes-returning boundary.
|
|
@@ -42,7 +42,7 @@ class CliRouter:
|
|
|
42
42
|
|
|
43
43
|
async def run(
|
|
44
44
|
self, ctx: CliContext, io: CliIO, start_node_id: str = "main"
|
|
45
|
-
) ->
|
|
45
|
+
) -> bool:
|
|
46
46
|
current = start_node_id
|
|
47
47
|
while True:
|
|
48
48
|
node = self.nodes[current]
|
|
@@ -57,7 +57,7 @@ class CliRouter:
|
|
|
57
57
|
|
|
58
58
|
if selection == "e":
|
|
59
59
|
io.write("Exiting.")
|
|
60
|
-
|
|
60
|
+
return True
|
|
61
61
|
if selection == "b":
|
|
62
62
|
if node.parent_id is None:
|
|
63
63
|
continue
|
|
@@ -92,9 +92,16 @@ class CliRouter:
|
|
|
92
92
|
io.write(f"Unknown command: {matched.command_id}")
|
|
93
93
|
continue
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
try:
|
|
96
|
+
result = await command.run(ctx, io)
|
|
97
|
+
except SystemExit as exc:
|
|
98
|
+
if exc.code == 0:
|
|
99
|
+
return True
|
|
100
|
+
raise
|
|
96
101
|
payload = result.payload
|
|
97
102
|
if result.status == "ok" and isinstance(payload, dict):
|
|
103
|
+
if payload.get("exit") is True:
|
|
104
|
+
return True
|
|
98
105
|
requested_node = payload.get("next_node_id")
|
|
99
106
|
if isinstance(requested_node, str):
|
|
100
107
|
if requested_node in self.nodes:
|
|
@@ -144,10 +144,14 @@ def main(*, debug: bool | None = None) -> None:
|
|
|
144
144
|
terminal_io = TerminalIO()
|
|
145
145
|
while True:
|
|
146
146
|
try:
|
|
147
|
-
asyncio.run(
|
|
147
|
+
should_exit = asyncio.run(
|
|
148
|
+
runtime.router.run(runtime, terminal_io, start_node_id="main")
|
|
149
|
+
)
|
|
148
150
|
except KeyboardInterrupt:
|
|
149
151
|
print("\nInterrupted. Use 'e' to exit.") # noqa: T201
|
|
150
152
|
continue
|
|
153
|
+
if should_exit:
|
|
154
|
+
return
|
|
151
155
|
|
|
152
156
|
|
|
153
157
|
__all__ = [
|
|
@@ -19,7 +19,9 @@ from axis.cli.packs.devices import (
|
|
|
19
19
|
)
|
|
20
20
|
from axis.device import AxisDevice
|
|
21
21
|
from axis.errors import RequestError
|
|
22
|
-
from axis.models.configuration import Configuration
|
|
22
|
+
from axis.models.configuration import Configuration, WebProtocol
|
|
23
|
+
from axis.rtsp import RTSPClient, Signal
|
|
24
|
+
from axis.websocket import WebSocketClient
|
|
23
25
|
|
|
24
26
|
if TYPE_CHECKING:
|
|
25
27
|
from axis.cli.core.context import CliContext
|
|
@@ -54,6 +56,31 @@ def _debug_dump(label: str, payload: object) -> None:
|
|
|
54
56
|
print(f"[debug] {label}:\n{pformat(payload)}") # noqa: T201
|
|
55
57
|
|
|
56
58
|
|
|
59
|
+
def _event_stream_details(device: AxisDevice) -> tuple[str, str]:
|
|
60
|
+
if device.stream.use_websocket:
|
|
61
|
+
if device.config.web_proto == WebProtocol.HTTPS:
|
|
62
|
+
verification = "enabled" if device.config.verify_ssl else "disabled"
|
|
63
|
+
return (
|
|
64
|
+
"WebSocket over WSS",
|
|
65
|
+
f"TLS encrypted; certificate verification {verification}; session-token authentication.",
|
|
66
|
+
)
|
|
67
|
+
return "WebSocket over WS", "Unencrypted; session-token authentication."
|
|
68
|
+
|
|
69
|
+
return "RTSP", "Unencrypted; authentication is negotiated when connected."
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _event_authentication_details(device: AxisDevice) -> str:
|
|
73
|
+
stream = device.stream.stream
|
|
74
|
+
if isinstance(stream, WebSocketClient):
|
|
75
|
+
return stream.authentication or "Negotiating"
|
|
76
|
+
if isinstance(stream, RTSPClient):
|
|
77
|
+
if stream.session.digest:
|
|
78
|
+
return "Digest"
|
|
79
|
+
if stream.session.basic:
|
|
80
|
+
return "Basic"
|
|
81
|
+
return "Negotiating"
|
|
82
|
+
|
|
83
|
+
|
|
57
84
|
def _render_events_node(ctx: CliContext, io: CliIO) -> None:
|
|
58
85
|
if ctx.selected_serial is None or ctx.selected_device is None:
|
|
59
86
|
io.write("\nNo selected device.")
|
|
@@ -176,6 +203,7 @@ async def _live_listen_async(
|
|
|
176
203
|
host=credentials["host"],
|
|
177
204
|
username=credentials["username"],
|
|
178
205
|
password=credentials["password"],
|
|
206
|
+
websocket_enabled=True,
|
|
179
207
|
)
|
|
180
208
|
device = AxisDevice(config)
|
|
181
209
|
|
|
@@ -186,13 +214,28 @@ async def _live_listen_async(
|
|
|
186
214
|
callback=_on_event,
|
|
187
215
|
id_filter=topic_filter,
|
|
188
216
|
)
|
|
217
|
+
|
|
218
|
+
def _on_connection_status(signal: Signal) -> None:
|
|
219
|
+
if signal == Signal.PLAYING:
|
|
220
|
+
print( # noqa: T201
|
|
221
|
+
f"Authentication: {_event_authentication_details(device)}"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
device.stream.connection_status_callback.append(_on_connection_status)
|
|
189
225
|
stop_event = asyncio.Event()
|
|
190
226
|
try:
|
|
227
|
+
await device.vapix.api_discovery.update()
|
|
191
228
|
device.enable_events()
|
|
229
|
+
protocol, security = _event_stream_details(device)
|
|
230
|
+
print(f"Event transport: {protocol}") # noqa: T201
|
|
231
|
+
print(f"Security: {security}") # noqa: T201
|
|
232
|
+
device.stream.start()
|
|
192
233
|
await stop_event.wait()
|
|
193
234
|
except asyncio.CancelledError:
|
|
194
235
|
pass
|
|
195
236
|
finally:
|
|
237
|
+
device.stream.connection_status_callback.remove(_on_connection_status)
|
|
238
|
+
device.stream.stop()
|
|
196
239
|
unsubscribe()
|
|
197
240
|
|
|
198
241
|
try:
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Firmware management API.
|
|
2
|
+
|
|
3
|
+
Listed in API discovery from AXIS OS 7.40, it replaces restart.cgi for
|
|
4
|
+
rebooting. Requires an administrator account.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ..models.api import ApiItem
|
|
8
|
+
from ..models.api_discovery import ApiId
|
|
9
|
+
from ..models.firmware_management import API_VERSION, RebootRequest
|
|
10
|
+
from .api_handler import ApiHandler
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FirmwareManagementHandler(ApiHandler[ApiItem]):
|
|
14
|
+
"""Firmware management of a device."""
|
|
15
|
+
|
|
16
|
+
api_id = ApiId.FIRMWARE_MANAGER
|
|
17
|
+
default_api_version = API_VERSION
|
|
18
|
+
|
|
19
|
+
async def reboot(self) -> None:
|
|
20
|
+
"""Reboot the device."""
|
|
21
|
+
await self.vapix.api_request(RebootRequest())
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Axis Vapix restart API.
|
|
2
|
+
|
|
3
|
+
Restart the device. Requires an administrator account. Superseded by the
|
|
4
|
+
firmware management API, use this on devices that don't list it.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ..models.api import ApiItem
|
|
8
|
+
from ..models.restart_cgi import RestartRequest
|
|
9
|
+
from .api_handler import ApiHandler
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RestartHandler(ApiHandler[ApiItem]):
|
|
13
|
+
"""Restart the device through restart.cgi."""
|
|
14
|
+
|
|
15
|
+
async def restart(self) -> None:
|
|
16
|
+
"""Restart the device."""
|
|
17
|
+
await self.vapix.api_request(RestartRequest())
|
|
@@ -10,7 +10,6 @@ import aiohttp
|
|
|
10
10
|
from ..errors import RequestError, raise_error
|
|
11
11
|
from ..models.configuration import AuthScheme
|
|
12
12
|
from ..models.pwdgrp_cgi import SecondaryGroup
|
|
13
|
-
from .aiohttp_digest import AiohttpDigestAuth
|
|
14
13
|
from .api_discovery import ApiDiscoveryHandler
|
|
15
14
|
from .api_handler import ApiHandler, HandlerGroup
|
|
16
15
|
from .applications import ApplicationsHandler
|
|
@@ -21,6 +20,7 @@ from .applications.object_analytics import ObjectAnalyticsHandler
|
|
|
21
20
|
from .applications.vmd4 import Vmd4Handler
|
|
22
21
|
from .basic_device_info import BasicDeviceInfoHandler
|
|
23
22
|
from .event_instances import EventInstanceHandler
|
|
23
|
+
from .firmware_management import FirmwareManagementHandler
|
|
24
24
|
from .light_control import LightHandler
|
|
25
25
|
from .mqtt import MqttClientHandler
|
|
26
26
|
from .parameters.param_cgi import Params
|
|
@@ -29,10 +29,12 @@ from .port_cgi import Ports
|
|
|
29
29
|
from .port_management import IoPortManagement
|
|
30
30
|
from .ptz import PtzControl
|
|
31
31
|
from .pwdgrp_cgi import Users
|
|
32
|
+
from .restart_cgi import RestartHandler
|
|
32
33
|
from .stream_profiles import StreamProfilesHandler
|
|
33
34
|
from .temperature_control import TemperatureControlHandler
|
|
34
35
|
from .user_groups import UserGroups
|
|
35
36
|
from .view_areas import ViewAreaHandler
|
|
37
|
+
from .virtual_input import VirtualInputHandler
|
|
36
38
|
|
|
37
39
|
if TYPE_CHECKING:
|
|
38
40
|
from collections.abc import Awaitable
|
|
@@ -68,16 +70,15 @@ class InterfaceState:
|
|
|
68
70
|
class Vapix:
|
|
69
71
|
"""Vapix parameter request."""
|
|
70
72
|
|
|
71
|
-
auth:
|
|
73
|
+
auth: str | None
|
|
72
74
|
|
|
73
75
|
def __init__(self, device: AxisDevice) -> None:
|
|
74
76
|
"""Store local reference to device config."""
|
|
75
77
|
self.device = device
|
|
76
78
|
self._aiohttp_digest_middleware: Any | None = None
|
|
77
|
-
self._aiohttp_digest_auth = AiohttpDigestAuth(device)
|
|
78
79
|
|
|
79
80
|
if device.config.auth_scheme == AuthScheme.BASIC:
|
|
80
|
-
self.auth = self.
|
|
81
|
+
self.auth = self._basic_auth_header()
|
|
81
82
|
else:
|
|
82
83
|
self.auth = None
|
|
83
84
|
self._aiohttp_digest_middleware = self._aiohttp_digest_middleware_obj()
|
|
@@ -95,6 +96,7 @@ class Vapix:
|
|
|
95
96
|
self.params: Params = Params(self)
|
|
96
97
|
|
|
97
98
|
self.basic_device_info = BasicDeviceInfoHandler(self)
|
|
99
|
+
self.firmware_management = FirmwareManagementHandler(self)
|
|
98
100
|
self.io_port_management = IoPortManagement(self)
|
|
99
101
|
self.light_control = LightHandler(self)
|
|
100
102
|
self.mqtt = MqttClientHandler(self)
|
|
@@ -105,6 +107,8 @@ class Vapix:
|
|
|
105
107
|
|
|
106
108
|
self.port_cgi = Ports(self)
|
|
107
109
|
self.ptz = PtzControl(self)
|
|
110
|
+
self.restart_cgi = RestartHandler(self)
|
|
111
|
+
self.virtual_input = VirtualInputHandler(self)
|
|
108
112
|
|
|
109
113
|
self.applications: ApplicationsHandler = ApplicationsHandler(self)
|
|
110
114
|
self.fence_guard = FenceGuardHandler(self)
|
|
@@ -124,6 +128,15 @@ class Vapix:
|
|
|
124
128
|
return self.params.property_handler["0"].firmware_version
|
|
125
129
|
return ""
|
|
126
130
|
|
|
131
|
+
@property
|
|
132
|
+
def product_full_name(self) -> str:
|
|
133
|
+
"""Product full name of device."""
|
|
134
|
+
if self.basic_device_info.initialized:
|
|
135
|
+
return self.basic_device_info["0"].product_full_name
|
|
136
|
+
if self.params.brand_handler.initialized:
|
|
137
|
+
return self.params.brand_handler["0"].product_full_name
|
|
138
|
+
return ""
|
|
139
|
+
|
|
127
140
|
@property
|
|
128
141
|
def product_number(self) -> str:
|
|
129
142
|
"""Product number of device."""
|
|
@@ -178,6 +191,8 @@ class Vapix:
|
|
|
178
191
|
"""Initialize Vapix functions."""
|
|
179
192
|
await self.initialize_api_discovery()
|
|
180
193
|
await self.initialize_param_cgi(preload_data=False)
|
|
194
|
+
if "Companion" in self.product_full_name:
|
|
195
|
+
self.device.config.is_companion = True
|
|
181
196
|
await self.initialize_applications()
|
|
182
197
|
|
|
183
198
|
async def initialize_api_discovery(self) -> None:
|
|
@@ -340,7 +355,7 @@ class Vapix:
|
|
|
340
355
|
) -> ApiResponseT:
|
|
341
356
|
"""Make a request and decode response based on the request contract."""
|
|
342
357
|
params = api_request.params or {}
|
|
343
|
-
if self.device.config.is_companion:
|
|
358
|
+
if self.device.config.is_companion and api_request.supports_companion_param:
|
|
344
359
|
params["Axis-Orig-Sw"] = "true"
|
|
345
360
|
bytes_data = await self.request(
|
|
346
361
|
method=api_request.method,
|
|
@@ -419,7 +434,7 @@ class Vapix:
|
|
|
419
434
|
|
|
420
435
|
if status_code >= 400:
|
|
421
436
|
if self._should_retry_with_basic(response_headers, allow_auto_basic_retry):
|
|
422
|
-
self.auth = self.
|
|
437
|
+
self.auth = self._basic_auth_header()
|
|
423
438
|
self._aiohttp_digest_middleware = None
|
|
424
439
|
return await self._request(
|
|
425
440
|
method=method,
|
|
@@ -458,16 +473,16 @@ class Vapix:
|
|
|
458
473
|
)
|
|
459
474
|
session = self.device.config.session
|
|
460
475
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
476
|
+
request_headers = dict(headers or {})
|
|
477
|
+
if self.auth is not None and not any(
|
|
478
|
+
header.lower() == "authorization" for header in request_headers
|
|
479
|
+
):
|
|
480
|
+
request_headers["Authorization"] = self.auth
|
|
465
481
|
|
|
466
482
|
request_kwargs: dict[str, Any] = {
|
|
467
483
|
"data": request_data,
|
|
468
|
-
"headers":
|
|
484
|
+
"headers": request_headers,
|
|
469
485
|
"params": params,
|
|
470
|
-
"auth": self.auth,
|
|
471
486
|
"timeout": TIME_OUT,
|
|
472
487
|
}
|
|
473
488
|
if self._aiohttp_digest_middleware is not None:
|
|
@@ -492,9 +507,9 @@ class Vapix:
|
|
|
492
507
|
password=self.device.config.password,
|
|
493
508
|
)
|
|
494
509
|
|
|
495
|
-
def
|
|
496
|
-
"""Create
|
|
497
|
-
return aiohttp.
|
|
510
|
+
def _basic_auth_header(self) -> str:
|
|
511
|
+
"""Create an HTTP Basic Authorization header value."""
|
|
512
|
+
return aiohttp.encode_basic_auth(
|
|
498
513
|
self.device.config.username, self.device.config.password
|
|
499
514
|
)
|
|
500
515
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Virtual input API.
|
|
2
|
+
|
|
3
|
+
Activate and deactivate virtual input ports, which can be used as start event
|
|
4
|
+
or condition in the device's action rules. Virtual inputs are not listed in API
|
|
5
|
+
discovery, support is announced by Properties.VirtualInput.VirtualInput. The
|
|
6
|
+
available ports and their state are only reported through the VirtualInput
|
|
7
|
+
event, which is left to the caller to combine with this API.
|
|
8
|
+
|
|
9
|
+
https://developer.axis.com/vapix/network-video/input-and-outputs/#virtual-input-api
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from ..models.api import ApiItem
|
|
13
|
+
from ..models.virtual_input import (
|
|
14
|
+
ActivateVirtualInputRequest,
|
|
15
|
+
DeactivateVirtualInputRequest,
|
|
16
|
+
)
|
|
17
|
+
from .api_handler import ApiHandler
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class VirtualInputHandler(ApiHandler[ApiItem]):
|
|
21
|
+
"""Virtual input ports of a device."""
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def listed_in_parameters(self) -> bool:
|
|
25
|
+
"""Is virtual input supported."""
|
|
26
|
+
if prop := self.vapix.params.property_handler.get("0"):
|
|
27
|
+
return prop.virtual_input
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
async def activate(self, port: int) -> bool:
|
|
31
|
+
"""Activate virtual input port, return True if the state changed."""
|
|
32
|
+
response = await self.vapix.api_request(ActivateVirtualInputRequest(port))
|
|
33
|
+
return response.data
|
|
34
|
+
|
|
35
|
+
async def deactivate(self, port: int) -> bool:
|
|
36
|
+
"""Deactivate virtual input port, return True if the state changed."""
|
|
37
|
+
response = await self.vapix.api_request(DeactivateVirtualInputRequest(port))
|
|
38
|
+
return response.data
|
|
@@ -68,6 +68,8 @@ class ApiRequest[ResponseT = ApiResponse[bytes]]:
|
|
|
68
68
|
path: str = field(init=False)
|
|
69
69
|
content_type: str = field(init=False)
|
|
70
70
|
response_type: ClassVar[type[ApiResponseDecoder[ResponseT]]]
|
|
71
|
+
# Some APIs reject the Axis-Orig-Sw parameter used by Companion devices
|
|
72
|
+
supports_companion_param: ClassVar[bool] = True
|
|
71
73
|
|
|
72
74
|
@property
|
|
73
75
|
def content(self) -> bytes | None:
|
|
@@ -229,6 +229,7 @@ class ListApisRequest(ApiRequest[GetAllApisResponse]):
|
|
|
229
229
|
path = "/axis-cgi/apidiscovery.cgi"
|
|
230
230
|
content_type = "application/json"
|
|
231
231
|
response_type = GetAllApisResponse
|
|
232
|
+
supports_companion_param = False
|
|
232
233
|
error_codes = error_codes
|
|
233
234
|
|
|
234
235
|
api_version: str = API_VERSION
|
|
@@ -276,6 +277,7 @@ class GetSupportedVersionsRequest(ApiRequest[GetSupportedVersionsResponse]):
|
|
|
276
277
|
path = "/axis-cgi/apidiscovery.cgi"
|
|
277
278
|
content_type = "application/json"
|
|
278
279
|
response_type = GetSupportedVersionsResponse
|
|
280
|
+
supports_companion_param = False
|
|
279
281
|
error_codes = error_codes
|
|
280
282
|
|
|
281
283
|
context: str = CONTEXT
|
|
@@ -151,6 +151,7 @@ class GetAllPropertiesRequest(ApiRequest[GetAllPropertiesResponse]):
|
|
|
151
151
|
path = "/axis-cgi/basicdeviceinfo.cgi"
|
|
152
152
|
content_type = "application/json"
|
|
153
153
|
response_type = GetAllPropertiesResponse
|
|
154
|
+
supports_companion_param = False
|
|
154
155
|
error_codes = error_codes
|
|
155
156
|
|
|
156
157
|
api_version: str = API_VERSION
|
|
@@ -198,6 +199,7 @@ class GetSupportedVersionsRequest(ApiRequest[GetSupportedVersionsResponse]):
|
|
|
198
199
|
path = "/axis-cgi/basicdeviceinfo.cgi"
|
|
199
200
|
content_type = "application/json"
|
|
200
201
|
response_type = GetSupportedVersionsResponse
|
|
202
|
+
supports_companion_param = False
|
|
201
203
|
error_codes = error_codes
|
|
202
204
|
|
|
203
205
|
context: str = CONTEXT
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Firmware management API.
|
|
2
|
+
|
|
3
|
+
Manage the firmware of the device. Only rebooting is supported here.
|
|
4
|
+
|
|
5
|
+
https://developer.axis.com/vapix/network-video/firmware-management-api/
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any, Self
|
|
10
|
+
|
|
11
|
+
import orjson
|
|
12
|
+
|
|
13
|
+
from ..errors import ResponseError
|
|
14
|
+
from .api import CONTEXT, ApiRequest, ApiResponse
|
|
15
|
+
|
|
16
|
+
API_VERSION = "1.0"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class RebootResponse(ApiResponse[None]):
|
|
21
|
+
"""Response from rebooting the device."""
|
|
22
|
+
|
|
23
|
+
data: None
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def decode(cls, bytes_data: bytes) -> Self:
|
|
27
|
+
"""Decode response, raise ResponseError if the device reports an error."""
|
|
28
|
+
msg = "Invalid firmware management response"
|
|
29
|
+
try:
|
|
30
|
+
data: Any = orjson.loads(bytes_data)
|
|
31
|
+
except orjson.JSONDecodeError as err:
|
|
32
|
+
raise ResponseError(msg) from err
|
|
33
|
+
if not isinstance(data, dict):
|
|
34
|
+
raise ResponseError(msg)
|
|
35
|
+
if isinstance(error := data.get("error"), dict):
|
|
36
|
+
msg = (
|
|
37
|
+
f"Firmware management error {error.get('code')}: {error.get('message')}"
|
|
38
|
+
)
|
|
39
|
+
raise ResponseError(msg)
|
|
40
|
+
return cls(data=None)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class RebootRequest(ApiRequest[RebootResponse]):
|
|
45
|
+
"""Request object for rebooting the device."""
|
|
46
|
+
|
|
47
|
+
method = "post"
|
|
48
|
+
path = "/axis-cgi/firmwaremanagement.cgi"
|
|
49
|
+
content_type = "application/json"
|
|
50
|
+
response_type = RebootResponse
|
|
51
|
+
|
|
52
|
+
api_version: str = API_VERSION
|
|
53
|
+
context: str = CONTEXT
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def content(self) -> bytes:
|
|
57
|
+
"""Initialize request data."""
|
|
58
|
+
return orjson.dumps(
|
|
59
|
+
{
|
|
60
|
+
"apiVersion": self.api_version,
|
|
61
|
+
"context": self.context,
|
|
62
|
+
"method": "reboot",
|
|
63
|
+
}
|
|
64
|
+
)
|
|
@@ -103,6 +103,12 @@ class PropertySystemParamT(TypedDict):
|
|
|
103
103
|
Soc: str
|
|
104
104
|
|
|
105
105
|
|
|
106
|
+
class PropertyVirtualInputParamT(TypedDict):
|
|
107
|
+
"""Represent a virtual input object."""
|
|
108
|
+
|
|
109
|
+
VirtualInput: bool
|
|
110
|
+
|
|
111
|
+
|
|
106
112
|
class PropertyParamT(TypedDict):
|
|
107
113
|
"""Represent a property object."""
|
|
108
114
|
|
|
@@ -113,6 +119,7 @@ class PropertyParamT(TypedDict):
|
|
|
113
119
|
LightControl: NotRequired[PropertyLightControlParamT]
|
|
114
120
|
PTZ: NotRequired[PropertyPtzParamT]
|
|
115
121
|
System: PropertySystemParamT
|
|
122
|
+
VirtualInput: NotRequired[PropertyVirtualInputParamT]
|
|
116
123
|
|
|
117
124
|
|
|
118
125
|
@dataclass(frozen=True)
|
|
@@ -174,6 +181,9 @@ class PropertyParam(ParamItem):
|
|
|
174
181
|
system_serial_number: str
|
|
175
182
|
"""Device serial number."""
|
|
176
183
|
|
|
184
|
+
virtual_input: bool
|
|
185
|
+
"""Support virtual input ports."""
|
|
186
|
+
|
|
177
187
|
@classmethod
|
|
178
188
|
def decode(cls, data: PropertyParamT) -> Self:
|
|
179
189
|
"""Decode dictionary to class object."""
|
|
@@ -200,4 +210,5 @@ class PropertyParam(ParamItem):
|
|
|
200
210
|
ptz=data.get("PTZ", {}).get("PTZ", False),
|
|
201
211
|
digital_ptz=data.get("PTZ", {}).get("DigitalPTZ", False),
|
|
202
212
|
system_serial_number=data["System"]["SerialNumber"],
|
|
213
|
+
virtual_input=data.get("VirtualInput", {}).get("VirtualInput", False),
|
|
203
214
|
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Restart API."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from .api import ApiRequest, ApiResponse, BytesResponse
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class RestartRequest(ApiRequest[ApiResponse[bytes]]):
|
|
10
|
+
"""Request object for restarting the device."""
|
|
11
|
+
|
|
12
|
+
method = "get"
|
|
13
|
+
path = "/axis-cgi/restart.cgi"
|
|
14
|
+
content_type = "text/plain"
|
|
15
|
+
response_type = BytesResponse
|