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