zm-py 0.5.5.dev13__tar.gz → 0.5.6.dev1__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.
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/PKG-INFO +16 -2
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/README.md +15 -1
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/pyproject.toml +1 -1
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/tests/README.md +4 -6
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/tests/e2e/conftest.py +2 -2
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/tests/e2e/test_e2e_auth.py +1 -1
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/tests/test_client.py +112 -0
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/zoneminder/monitor.py +70 -3
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/zoneminder/zm.py +20 -0
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/LICENSE.md +0 -0
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/tests/__init__.py +0 -0
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/tests/e2e/__init__.py +0 -0
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/tests/e2e/test_e2e_api_probes.py +0 -0
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/tests/e2e/test_e2e_availability.py +0 -0
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/tests/e2e/test_e2e_monitors.py +0 -0
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/tests/e2e/test_e2e_ptz.py +0 -0
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/tests/e2e/test_e2e_servers.py +0 -0
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/tests/e2e/test_e2e_states.py +0 -0
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/tests/test_enums.py +0 -0
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/tests/test_exceptions.py +0 -0
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/tests/test_monitor.py +0 -0
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/tests/test_ptz.py +0 -0
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/tests/test_run_state.py +0 -0
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/tests/test_server.py +0 -0
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/tests/test_zm.py +0 -0
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/zoneminder/__init__.py +0 -0
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/zoneminder/exceptions.py +0 -0
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/zoneminder/run_state.py +0 -0
- {zm_py-0.5.5.dev13 → zm_py-0.5.6.dev1}/zoneminder/server.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zm-py
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.6.dev1
|
|
4
4
|
Summary: A loose python wrapper around the ZoneMinder REST API.
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
License-File: LICENSE.md
|
|
@@ -55,6 +55,18 @@ Historical sources and authorship information is available as part of the Home A
|
|
|
55
55
|
- [ZoneMinder Sensor](https://github.com/home-assistant/home-assistant/commits/dev/homeassistant/components/sensor/zoneminder.py)
|
|
56
56
|
- [ZoneMinder Switch](https://github.com/home-assistant/home-assistant/commits/dev/homeassistant/components/switch/zoneminder.py)
|
|
57
57
|
|
|
58
|
+
## Documentation
|
|
59
|
+
|
|
60
|
+
- **[API Reference](docs/api.md)** — all public classes, methods, properties, and enums
|
|
61
|
+
- **[Architecture](docs/architecture.md)** — package structure, design patterns, caching strategy, Mermaid diagrams
|
|
62
|
+
- Architecture diagrams:
|
|
63
|
+
- [Package architecture](docs/architecture-package-architecture.png)
|
|
64
|
+
- [Class relationships](docs/architecture-class-relationships.png)
|
|
65
|
+
- [Auth & request flow](docs/architecture-auth-amp-request-flow.png)
|
|
66
|
+
- [Multi-server URL routing](docs/architecture-multi-server-url-routing.png)
|
|
67
|
+
- [ZoneMinder API endpoints](docs/api-overview-zoneminder-api-endpoints.png)
|
|
68
|
+
- [Monitor state model](docs/api-overview-monitor-state-model.png)
|
|
69
|
+
|
|
58
70
|
## Installation
|
|
59
71
|
|
|
60
72
|
### PyPI
|
|
@@ -78,7 +90,9 @@ zm_client = ZoneMinder(
|
|
|
78
90
|
server_path=SERVER_PATH,
|
|
79
91
|
username=USER,
|
|
80
92
|
password=PASS,
|
|
81
|
-
verify_ssl=False
|
|
93
|
+
verify_ssl=False,
|
|
94
|
+
stream_scale=50, # optional: scale streams to 50%
|
|
95
|
+
stream_maxfps=5.0, # optional: cap MJPEG at 5 FPS
|
|
82
96
|
)
|
|
83
97
|
|
|
84
98
|
# Zoneminder authentication
|
|
@@ -30,6 +30,18 @@ Historical sources and authorship information is available as part of the Home A
|
|
|
30
30
|
- [ZoneMinder Sensor](https://github.com/home-assistant/home-assistant/commits/dev/homeassistant/components/sensor/zoneminder.py)
|
|
31
31
|
- [ZoneMinder Switch](https://github.com/home-assistant/home-assistant/commits/dev/homeassistant/components/switch/zoneminder.py)
|
|
32
32
|
|
|
33
|
+
## Documentation
|
|
34
|
+
|
|
35
|
+
- **[API Reference](docs/api.md)** — all public classes, methods, properties, and enums
|
|
36
|
+
- **[Architecture](docs/architecture.md)** — package structure, design patterns, caching strategy, Mermaid diagrams
|
|
37
|
+
- Architecture diagrams:
|
|
38
|
+
- [Package architecture](docs/architecture-package-architecture.png)
|
|
39
|
+
- [Class relationships](docs/architecture-class-relationships.png)
|
|
40
|
+
- [Auth & request flow](docs/architecture-auth-amp-request-flow.png)
|
|
41
|
+
- [Multi-server URL routing](docs/architecture-multi-server-url-routing.png)
|
|
42
|
+
- [ZoneMinder API endpoints](docs/api-overview-zoneminder-api-endpoints.png)
|
|
43
|
+
- [Monitor state model](docs/api-overview-monitor-state-model.png)
|
|
44
|
+
|
|
33
45
|
## Installation
|
|
34
46
|
|
|
35
47
|
### PyPI
|
|
@@ -53,7 +65,9 @@ zm_client = ZoneMinder(
|
|
|
53
65
|
server_path=SERVER_PATH,
|
|
54
66
|
username=USER,
|
|
55
67
|
password=PASS,
|
|
56
|
-
verify_ssl=False
|
|
68
|
+
verify_ssl=False,
|
|
69
|
+
stream_scale=50, # optional: scale streams to 50%
|
|
70
|
+
stream_maxfps=5.0, # optional: cap MJPEG at 5 FPS
|
|
57
71
|
)
|
|
58
72
|
|
|
59
73
|
# Zoneminder authentication
|
|
@@ -87,6 +87,7 @@ ZM_E2E_WRITE=1 pytest -m zm_e2e_write tests/e2e/ -v
|
|
|
87
87
|
| `test_run_state.py` | RunState construction, properties, `active` parsing, `activate` |
|
|
88
88
|
| `test_enums.py` | `TimePeriod` (period/title/get_time_period), `MonitorState`, `ControlType` values |
|
|
89
89
|
| `test_exceptions.py` | Exception hierarchy, `__str__` formatting |
|
|
90
|
+
| `test_server.py` | Server construction, properties, ZMS URL building, base URL building |
|
|
90
91
|
| `test_client.py` | `verify_ssl`, `is_available` parsing, `get_monitors`/`get_run_states`/`get_active_state` response handling, `move_monitor` exception swallowing |
|
|
91
92
|
|
|
92
93
|
## E2E Test Modules
|
|
@@ -98,7 +99,8 @@ ZM_E2E_WRITE=1 pytest -m zm_e2e_write tests/e2e/ -v
|
|
|
98
99
|
| `test_e2e_states.py` | Run states listing, active state, state switching | `api/states.json`, `api/states/change/{name}.json` |
|
|
99
100
|
| `test_e2e_availability.py` | Daemon check, get_state/change_state plumbing, ZMS URL, auth URL helpers | `api/host/daemonCheck.json`, `api/host/getVersion.json` |
|
|
100
101
|
| `test_e2e_ptz.py` | PTZ control on controllable/non-controllable monitors | `index.php` (control view) |
|
|
101
|
-
| `
|
|
102
|
+
| `test_e2e_servers.py` | Server listing, server URLs, monitor ServerId probe | `api/servers.json` |
|
|
103
|
+
| `test_e2e_api_probes.py` | Raw API response inspection -- documents actual types/shapes from ZM 1.38.x to validate bugs | All endpoints |
|
|
102
104
|
|
|
103
105
|
## API Coverage
|
|
104
106
|
|
|
@@ -117,6 +119,7 @@ GET api/states/change/{name}.json -> ZoneMinder.set_active_state() [write]
|
|
|
117
119
|
GET api/monitors/alarm/... -> Monitor.is_recording
|
|
118
120
|
GET api/monitors/daemonStatus/... -> Monitor.is_available
|
|
119
121
|
GET api/events/consoleEvents/... -> Monitor.get_events()
|
|
122
|
+
GET api/servers.json -> ZoneMinder.get_servers()
|
|
120
123
|
POST index.php (PTZ control) -> Monitor.ptz_control_command() [write]
|
|
121
124
|
```
|
|
122
125
|
|
|
@@ -146,11 +149,6 @@ After the test run, a summary section is printed showing:
|
|
|
146
149
|
- Test target monitors
|
|
147
150
|
- Skip reasons and counts
|
|
148
151
|
|
|
149
|
-
## Known Bugs
|
|
150
|
-
|
|
151
|
-
See [BUGS.md](BUGS.md) for bugs discovered in zm-py during test development.
|
|
152
|
-
These are **not fixed** -- they are documented to guide future refactoring.
|
|
153
|
-
|
|
154
152
|
## Comparison with Sister Projects
|
|
155
153
|
|
|
156
154
|
This test suite follows the same E2E patterns established in:
|
|
@@ -202,8 +202,8 @@ def raw_session(zm_client: ZoneMinder):
|
|
|
202
202
|
s.verify = zm_client._verify_ssl
|
|
203
203
|
if zm_client._auth_token:
|
|
204
204
|
s.params = {"token": zm_client._auth_token}
|
|
205
|
-
elif zm_client.
|
|
206
|
-
s.cookies = zm_client.
|
|
205
|
+
elif zm_client._session.cookies:
|
|
206
|
+
s.cookies = zm_client._session.cookies
|
|
207
207
|
return s
|
|
208
208
|
|
|
209
209
|
|
|
@@ -27,7 +27,7 @@ class TestLogin:
|
|
|
27
27
|
"""After login(), the client should have an auth token or cookies."""
|
|
28
28
|
zm_client_fresh.login()
|
|
29
29
|
has_token = zm_client_fresh._auth_token is not None
|
|
30
|
-
has_cookies = zm_client_fresh.
|
|
30
|
+
has_cookies = len(zm_client_fresh._session.cookies) > 0
|
|
31
31
|
assert has_token or has_cookies, (
|
|
32
32
|
"Expected either JWT token or session cookies after login"
|
|
33
33
|
)
|
|
@@ -331,6 +331,118 @@ class TestMoveMonitor:
|
|
|
331
331
|
c._session.post.assert_called_once()
|
|
332
332
|
|
|
333
333
|
|
|
334
|
+
# ---------------------------------------------------------------------------
|
|
335
|
+
# goto_preset
|
|
336
|
+
# ---------------------------------------------------------------------------
|
|
337
|
+
|
|
338
|
+
class TestGotoPreset:
|
|
339
|
+
def _make_monitor(self, controllable=True):
|
|
340
|
+
raw = _monitor_raw(controllable="1" if controllable else "0")
|
|
341
|
+
return Monitor(_client(), raw)
|
|
342
|
+
|
|
343
|
+
def test_delegates_to_preset_command(self):
|
|
344
|
+
c = _client()
|
|
345
|
+
c._auth_token = "test-token"
|
|
346
|
+
c._session = MagicMock()
|
|
347
|
+
c._session.post.return_value = MagicMock(ok=True)
|
|
348
|
+
mon = Monitor(c, _monitor_raw(controllable="1"))
|
|
349
|
+
c.goto_preset(mon, 3)
|
|
350
|
+
c._session.post.assert_called_once()
|
|
351
|
+
|
|
352
|
+
def test_preset_params_contain_preset_number(self):
|
|
353
|
+
c = _client()
|
|
354
|
+
c._auth_token = "tok"
|
|
355
|
+
c._session = MagicMock()
|
|
356
|
+
c._session.post.return_value = MagicMock(ok=True)
|
|
357
|
+
mon = Monitor(c, _monitor_raw(controllable="1"))
|
|
358
|
+
c.goto_preset(mon, 3)
|
|
359
|
+
call_kwargs = c._session.post.call_args
|
|
360
|
+
assert call_kwargs.kwargs["params"]["control"] == "presetGoto3"
|
|
361
|
+
|
|
362
|
+
def test_raises_on_non_controllable(self):
|
|
363
|
+
c = _client()
|
|
364
|
+
c._auth_token = "tok"
|
|
365
|
+
mon = self._make_monitor(controllable=False)
|
|
366
|
+
with pytest.raises(MonitorControlTypeError):
|
|
367
|
+
c.goto_preset(mon, 1)
|
|
368
|
+
|
|
369
|
+
def test_returns_bool_on_success(self):
|
|
370
|
+
c = _client()
|
|
371
|
+
c._auth_token = "tok"
|
|
372
|
+
c._session = MagicMock()
|
|
373
|
+
c._session.post.return_value = MagicMock(ok=True)
|
|
374
|
+
mon = Monitor(c, _monitor_raw(controllable="1"))
|
|
375
|
+
result = c.goto_preset(mon, 5)
|
|
376
|
+
assert result is True
|
|
377
|
+
|
|
378
|
+
def test_returns_false_on_connection_error(self):
|
|
379
|
+
c = _client()
|
|
380
|
+
c._auth_token = "tok"
|
|
381
|
+
c._session = MagicMock()
|
|
382
|
+
c._session.post.side_effect = requests.exceptions.ConnectionError("refused")
|
|
383
|
+
mon = Monitor(c, _monitor_raw(controllable="1"))
|
|
384
|
+
result = c.goto_preset(mon, 1)
|
|
385
|
+
assert result is False
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
# ---------------------------------------------------------------------------
|
|
389
|
+
# goto_home
|
|
390
|
+
# ---------------------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
class TestGotoHome:
|
|
393
|
+
def _make_monitor(self, controllable=True):
|
|
394
|
+
raw = _monitor_raw(controllable="1" if controllable else "0")
|
|
395
|
+
return Monitor(_client(), raw)
|
|
396
|
+
|
|
397
|
+
def test_home_command_sends_preset_home(self):
|
|
398
|
+
c = _client()
|
|
399
|
+
c._auth_token = "tok"
|
|
400
|
+
c._session = MagicMock()
|
|
401
|
+
c._session.post.return_value = MagicMock(ok=True)
|
|
402
|
+
mon = Monitor(c, _monitor_raw(controllable="1"))
|
|
403
|
+
c.goto_home(mon)
|
|
404
|
+
call_kwargs = c._session.post.call_args
|
|
405
|
+
assert call_kwargs.kwargs["params"]["control"] == "presetHome"
|
|
406
|
+
|
|
407
|
+
def test_raises_on_non_controllable(self):
|
|
408
|
+
c = _client()
|
|
409
|
+
c._auth_token = "tok"
|
|
410
|
+
mon = self._make_monitor(controllable=False)
|
|
411
|
+
with pytest.raises(MonitorControlTypeError):
|
|
412
|
+
c.goto_home(mon)
|
|
413
|
+
|
|
414
|
+
def test_returns_bool_on_success(self):
|
|
415
|
+
c = _client()
|
|
416
|
+
c._auth_token = "tok"
|
|
417
|
+
c._session = MagicMock()
|
|
418
|
+
c._session.post.return_value = MagicMock(ok=True)
|
|
419
|
+
mon = Monitor(c, _monitor_raw(controllable="1"))
|
|
420
|
+
result = c.goto_home(mon)
|
|
421
|
+
assert result is True
|
|
422
|
+
|
|
423
|
+
def test_returns_false_on_connection_error(self):
|
|
424
|
+
c = _client()
|
|
425
|
+
c._auth_token = "tok"
|
|
426
|
+
c._session = MagicMock()
|
|
427
|
+
c._session.post.side_effect = requests.exceptions.ConnectionError("refused")
|
|
428
|
+
mon = Monitor(c, _monitor_raw(controllable="1"))
|
|
429
|
+
result = c.goto_home(mon)
|
|
430
|
+
assert result is False
|
|
431
|
+
|
|
432
|
+
def test_uses_server_url_for_multi_server(self):
|
|
433
|
+
"""goto_home should resolve the per-server URL."""
|
|
434
|
+
c = _client()
|
|
435
|
+
c._auth_token = "tok"
|
|
436
|
+
c._session = MagicMock()
|
|
437
|
+
c._session.post.return_value = MagicMock(ok=True)
|
|
438
|
+
raw = {"servers": [_server_raw(2, "Srv2", hostname="zm2.test", protocol="https")]}
|
|
439
|
+
with patch.object(c, "get_state", return_value=raw):
|
|
440
|
+
mon = Monitor(c, _monitor_raw(controllable="1", server_id="2"))
|
|
441
|
+
c.goto_home(mon)
|
|
442
|
+
call_kwargs = c._session.post.call_args
|
|
443
|
+
assert call_kwargs.kwargs["url"] == "https://zm2.test/zm/index.php"
|
|
444
|
+
|
|
445
|
+
|
|
334
446
|
class TestStaleTokenRetry:
|
|
335
447
|
"""Verify _zm_request recomputes token suffix after login() refreshes the token."""
|
|
336
448
|
|
|
@@ -5,12 +5,16 @@ from __future__ import annotations
|
|
|
5
5
|
from enum import Enum
|
|
6
6
|
import logging
|
|
7
7
|
import time
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
8
9
|
from urllib.parse import urlencode
|
|
9
10
|
|
|
10
11
|
import requests
|
|
11
12
|
|
|
12
13
|
from .exceptions import ControlTypeError, MonitorControlTypeError
|
|
13
14
|
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from zoneminder.zm import ZoneMinder
|
|
17
|
+
|
|
14
18
|
_LOGGER = logging.getLogger(__name__)
|
|
15
19
|
|
|
16
20
|
# Alarm state values as returned by the ZM API alarm status endpoint.
|
|
@@ -195,12 +199,19 @@ class TimePeriod(Enum):
|
|
|
195
199
|
class Monitor:
|
|
196
200
|
"""Represents a Monitor from ZoneMinder."""
|
|
197
201
|
|
|
198
|
-
def __init__(
|
|
202
|
+
def __init__(
|
|
203
|
+
self,
|
|
204
|
+
client: ZoneMinder,
|
|
205
|
+
raw_result: dict,
|
|
206
|
+
*,
|
|
207
|
+
scale: int | None = None,
|
|
208
|
+
maxfps: float | None = None,
|
|
209
|
+
) -> None:
|
|
199
210
|
"""Create a new Monitor."""
|
|
200
211
|
self._client = client
|
|
201
212
|
self._raw_result = raw_result
|
|
202
|
-
self._scale
|
|
203
|
-
self._maxfps
|
|
213
|
+
self._scale = scale
|
|
214
|
+
self._maxfps = maxfps
|
|
204
215
|
self._last_update = time.monotonic()
|
|
205
216
|
raw_monitor = raw_result["Monitor"]
|
|
206
217
|
self._monitor_id = int(raw_monitor["Id"])
|
|
@@ -476,3 +487,59 @@ class Monitor:
|
|
|
476
487
|
_LOGGER.exception("Unable to connect to ZoneMinder for PTZ control")
|
|
477
488
|
return False
|
|
478
489
|
return bool(req.ok)
|
|
490
|
+
|
|
491
|
+
def preset_command(
|
|
492
|
+
self, preset: int, token: str | None, base_url: str, cookies: dict | None = None
|
|
493
|
+
) -> bool:
|
|
494
|
+
"""Move camera to a numbered preset position."""
|
|
495
|
+
if not self.controllable:
|
|
496
|
+
raise MonitorControlTypeError()
|
|
497
|
+
|
|
498
|
+
ptz_url = f"{base_url}index.php"
|
|
499
|
+
params: dict[str, str | int] = {
|
|
500
|
+
"view": "request",
|
|
501
|
+
"request": "control",
|
|
502
|
+
"id": self.id,
|
|
503
|
+
"control": f"presetGoto{preset}",
|
|
504
|
+
}
|
|
505
|
+
if token:
|
|
506
|
+
params["token"] = token
|
|
507
|
+
|
|
508
|
+
try:
|
|
509
|
+
req = self._client._session.post( # pylint: disable=protected-access
|
|
510
|
+
url=ptz_url,
|
|
511
|
+
params=params,
|
|
512
|
+
cookies=cookies,
|
|
513
|
+
timeout=10,
|
|
514
|
+
)
|
|
515
|
+
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
|
|
516
|
+
_LOGGER.exception("Unable to connect to ZoneMinder for preset control")
|
|
517
|
+
return False
|
|
518
|
+
return bool(req.ok)
|
|
519
|
+
|
|
520
|
+
def home_command(self, token: str | None, base_url: str, cookies: dict | None = None) -> bool:
|
|
521
|
+
"""Move camera to the home position."""
|
|
522
|
+
if not self.controllable:
|
|
523
|
+
raise MonitorControlTypeError()
|
|
524
|
+
|
|
525
|
+
ptz_url = f"{base_url}index.php"
|
|
526
|
+
params: dict[str, str | int] = {
|
|
527
|
+
"view": "request",
|
|
528
|
+
"request": "control",
|
|
529
|
+
"id": self.id,
|
|
530
|
+
"control": "presetHome",
|
|
531
|
+
}
|
|
532
|
+
if token:
|
|
533
|
+
params["token"] = token
|
|
534
|
+
|
|
535
|
+
try:
|
|
536
|
+
req = self._client._session.post( # pylint: disable=protected-access
|
|
537
|
+
url=ptz_url,
|
|
538
|
+
params=params,
|
|
539
|
+
cookies=cookies,
|
|
540
|
+
timeout=10,
|
|
541
|
+
)
|
|
542
|
+
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
|
|
543
|
+
_LOGGER.exception("Unable to connect to ZoneMinder for home control")
|
|
544
|
+
return False
|
|
545
|
+
return bool(req.ok)
|
|
@@ -474,3 +474,23 @@ class ZoneMinder:
|
|
|
474
474
|
else:
|
|
475
475
|
_LOGGER.error("Failed to move camera to %s", direction)
|
|
476
476
|
return result
|
|
477
|
+
|
|
478
|
+
def goto_preset(self, monitor: Monitor, preset: int) -> bool:
|
|
479
|
+
"""Move camera to a numbered preset position."""
|
|
480
|
+
base_url = self.get_server_url_for_monitor(monitor.raw_monitor)
|
|
481
|
+
result = monitor.preset_command(preset, self._auth_token, base_url)
|
|
482
|
+
if result:
|
|
483
|
+
_LOGGER.info("Successfully moved camera to preset %d", preset)
|
|
484
|
+
else:
|
|
485
|
+
_LOGGER.error("Failed to move camera to preset %d", preset)
|
|
486
|
+
return result
|
|
487
|
+
|
|
488
|
+
def goto_home(self, monitor: Monitor) -> bool:
|
|
489
|
+
"""Move camera to the home position."""
|
|
490
|
+
base_url = self.get_server_url_for_monitor(monitor.raw_monitor)
|
|
491
|
+
result = monitor.home_command(self._auth_token, base_url)
|
|
492
|
+
if result:
|
|
493
|
+
_LOGGER.info("Successfully moved camera to home position")
|
|
494
|
+
else:
|
|
495
|
+
_LOGGER.error("Failed to move camera to home position")
|
|
496
|
+
return result
|
|
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
|