harbor-python 1.1.0__py3-none-any.whl → 1.2.1__py3-none-any.whl
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.
- harbor/__init__.py +2 -0
- harbor/core.py +39 -2
- harbor/data/mqtt_models.py +11 -3
- harbor/devices/camera.py +25 -14
- harbor/events.py +2 -2
- harbor/mqtt.py +167 -2
- {harbor_python-1.1.0.dist-info → harbor_python-1.2.1.dist-info}/METADATA +1 -1
- harbor_python-1.2.1.dist-info/RECORD +17 -0
- {harbor_python-1.1.0.dist-info → harbor_python-1.2.1.dist-info}/WHEEL +1 -1
- harbor_python-1.1.0.dist-info/RECORD +0 -17
- {harbor_python-1.1.0.dist-info → harbor_python-1.2.1.dist-info}/licenses/LICENSE +0 -0
harbor/__init__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from .config import HarborCameraConfig
|
|
2
2
|
from .core import Harbor
|
|
3
3
|
from .data.mqtt_models import (
|
|
4
|
+
GetCameraSettingsRequest,
|
|
4
5
|
HeartbeatEvent,
|
|
5
6
|
LocalLivekitHeartbeatEvent,
|
|
6
7
|
MotionDetectedEvent,
|
|
@@ -49,6 +50,7 @@ __all__ = [
|
|
|
49
50
|
"MotionDetectedUpdate",
|
|
50
51
|
"ViewerInfo",
|
|
51
52
|
"parse_message",
|
|
53
|
+
"GetCameraSettingsRequest",
|
|
52
54
|
"HeartbeatEvent",
|
|
53
55
|
"LocalLivekitHeartbeatEvent",
|
|
54
56
|
"SettingsEvent",
|
harbor/core.py
CHANGED
|
@@ -2,8 +2,9 @@ import logging
|
|
|
2
2
|
from typing import Any
|
|
3
3
|
|
|
4
4
|
from .config import HarborCameraConfig
|
|
5
|
+
from .data.mqtt_models import SettingsEvent
|
|
5
6
|
from .device import HarborDevice
|
|
6
|
-
from .mqtt import HarborMQTTClient
|
|
7
|
+
from .mqtt import DEFAULT_INITIAL_COMMANDS, HarborMQTTClient
|
|
7
8
|
|
|
8
9
|
_LOGGER = logging.getLogger(__name__)
|
|
9
10
|
|
|
@@ -32,12 +33,13 @@ class Harbor:
|
|
|
32
33
|
if config.serial in self._clients:
|
|
33
34
|
_LOGGER.warning("Camera connection already exists: %s", config.serial)
|
|
34
35
|
return
|
|
35
|
-
topics = list(self._topics_cache)
|
|
36
|
+
topics = list({*self._topics_cache, f"cameras/{config.serial}/responses/#"})
|
|
36
37
|
client = HarborMQTTClient(
|
|
37
38
|
config=config,
|
|
38
39
|
topics=topics,
|
|
39
40
|
message_handler=self.handle_message,
|
|
40
41
|
client_id=f"harbor-client-{config.serial}",
|
|
42
|
+
initial_commands=DEFAULT_INITIAL_COMMANDS,
|
|
41
43
|
)
|
|
42
44
|
self._clients[config.serial] = client
|
|
43
45
|
_LOGGER.info("Added MQTT client for camera: %s", config.serial)
|
|
@@ -54,6 +56,35 @@ class Harbor:
|
|
|
54
56
|
for device in self._devices.values():
|
|
55
57
|
device.shutdown()
|
|
56
58
|
|
|
59
|
+
async def publish_camera_command(
|
|
60
|
+
self,
|
|
61
|
+
serial: str,
|
|
62
|
+
command: str,
|
|
63
|
+
payload: Any,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Publish a command to a camera."""
|
|
66
|
+
await self._get_client(serial).publish_command(command, payload)
|
|
67
|
+
|
|
68
|
+
async def request_camera_command(
|
|
69
|
+
self,
|
|
70
|
+
serial: str,
|
|
71
|
+
command: str,
|
|
72
|
+
payload: dict[str, Any],
|
|
73
|
+
*,
|
|
74
|
+
timeout: float = 10.0,
|
|
75
|
+
) -> Any:
|
|
76
|
+
"""Publish a command to a camera and wait for the matching response."""
|
|
77
|
+
return await self._get_client(serial).request_command(command, payload, timeout=timeout)
|
|
78
|
+
|
|
79
|
+
async def get_camera_settings(
|
|
80
|
+
self,
|
|
81
|
+
serial: str,
|
|
82
|
+
*,
|
|
83
|
+
timeout: float = 10.0,
|
|
84
|
+
) -> SettingsEvent:
|
|
85
|
+
"""Request the camera settings payload."""
|
|
86
|
+
return await self._get_client(serial).get_settings(timeout=timeout)
|
|
87
|
+
|
|
57
88
|
async def handle_message(self, topic: str, payload: Any) -> None:
|
|
58
89
|
"""
|
|
59
90
|
Central message handler.
|
|
@@ -61,3 +92,9 @@ class Harbor:
|
|
|
61
92
|
"""
|
|
62
93
|
for device in self._devices.values():
|
|
63
94
|
await device.handle_message(topic, payload)
|
|
95
|
+
|
|
96
|
+
def _get_client(self, serial: str) -> HarborMQTTClient:
|
|
97
|
+
try:
|
|
98
|
+
return self._clients[serial]
|
|
99
|
+
except KeyError as exc:
|
|
100
|
+
raise KeyError(f"No camera connection exists for serial {serial!r}") from exc
|
harbor/data/mqtt_models.py
CHANGED
|
@@ -8,7 +8,7 @@ from pydantic import BaseModel, ConfigDict, Field
|
|
|
8
8
|
class HarborMQTTPayload(BaseModel):
|
|
9
9
|
"""Base model for Harbor MQTT payloads."""
|
|
10
10
|
|
|
11
|
-
model_config = ConfigDict(extra="allow")
|
|
11
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class LocalLivekitHeartbeatEvent(HarborMQTTPayload):
|
|
@@ -82,14 +82,22 @@ class SettingsEvent(HarborMQTTPayload):
|
|
|
82
82
|
"""Payload for a settings event."""
|
|
83
83
|
|
|
84
84
|
client: str | None = None
|
|
85
|
-
is_updating: bool | None = None
|
|
85
|
+
is_updating: bool | None = Field(default=None, alias="isUpdating")
|
|
86
86
|
seq: str | None = None
|
|
87
87
|
settings: Settings | None = None
|
|
88
88
|
state: SettingsState | None = None
|
|
89
|
-
triggered_by: str | None = None
|
|
89
|
+
triggered_by: str | None = Field(default=None, alias="triggeredBy")
|
|
90
90
|
updated: dict[str, Any] = Field(default_factory=dict)
|
|
91
91
|
|
|
92
92
|
|
|
93
|
+
class GetCameraSettingsRequest(HarborMQTTPayload):
|
|
94
|
+
"""Payload for the get-settings camera command."""
|
|
95
|
+
|
|
96
|
+
seq: str
|
|
97
|
+
client: str
|
|
98
|
+
triggered_by: str = Field(alias="triggeredBy")
|
|
99
|
+
|
|
100
|
+
|
|
93
101
|
class ViewerJoinedEvent(HarborMQTTPayload):
|
|
94
102
|
"""Payload for a viewer joined event."""
|
|
95
103
|
|
harbor/devices/camera.py
CHANGED
|
@@ -23,9 +23,10 @@ DEFAULT_EVENT_ACTIVE_SECONDS = 5.0
|
|
|
23
23
|
# Known values for enumerated state fields. The device reports these in
|
|
24
24
|
# mixed/upper case (e.g. "PLAYING", "GOOD"); they are normalized to the
|
|
25
25
|
# lowercase values below before being stored in ``HarborDeviceState.values``.
|
|
26
|
-
# A value outside these sets is
|
|
27
|
-
# so
|
|
28
|
-
#
|
|
26
|
+
# A value outside these sets is mapped to ``UNKNOWN_ENUM_VALUE`` (and logged
|
|
27
|
+
# once), so a stored enum value is always a member of its set. Consumers can
|
|
28
|
+
# rely on that guarantee instead of clamping the value themselves.
|
|
29
|
+
UNKNOWN_ENUM_VALUE = "unknown"
|
|
29
30
|
SPEAKER_STATES = frozenset({"idle", "muted", "off", "paused", "playing", "unknown"})
|
|
30
31
|
STREAM_QUALITIES = frozenset({"excellent", "fair", "good", "poor", "unknown"})
|
|
31
32
|
|
|
@@ -46,7 +47,10 @@ class HarborCamera(HarborDevice):
|
|
|
46
47
|
def get_topics(self) -> list[str]:
|
|
47
48
|
"""Return topics that should be subscribed for this device."""
|
|
48
49
|
|
|
49
|
-
return [
|
|
50
|
+
return [
|
|
51
|
+
f"cameras/{self.serial}/events/#",
|
|
52
|
+
f"cameras/{self.serial}/responses/#",
|
|
53
|
+
]
|
|
50
54
|
|
|
51
55
|
def _apply_event(self, event: HarborEvent) -> None:
|
|
52
56
|
"""Apply a Harbor event to camera state, including camera-only events."""
|
|
@@ -99,21 +103,28 @@ class HarborCamera(HarborDevice):
|
|
|
99
103
|
value: str | None,
|
|
100
104
|
known_values: frozenset[str],
|
|
101
105
|
) -> str | None:
|
|
102
|
-
"""Normalize an enumerated device value to a
|
|
106
|
+
"""Normalize an enumerated device value to a known lowercase member.
|
|
107
|
+
|
|
108
|
+
Returns ``None`` only when the device omitted the field. A value that
|
|
109
|
+
does not match ``known_values`` is mapped to ``UNKNOWN_ENUM_VALUE`` so
|
|
110
|
+
callers can rely on the result always being a member of the set.
|
|
111
|
+
"""
|
|
103
112
|
if value is None:
|
|
104
113
|
return None
|
|
105
114
|
normalized = value.strip().lower()
|
|
106
115
|
if not normalized:
|
|
107
116
|
return None
|
|
108
|
-
if normalized not in known_values
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
+
if normalized not in known_values:
|
|
118
|
+
if (field_name, normalized) not in self._unexpected_enum_values:
|
|
119
|
+
self._unexpected_enum_values.add((field_name, normalized))
|
|
120
|
+
_LOGGER.warning(
|
|
121
|
+
"Camera %s reported unexpected %s value %r (known values: %s)",
|
|
122
|
+
self.serial,
|
|
123
|
+
field_name,
|
|
124
|
+
normalized,
|
|
125
|
+
sorted(known_values),
|
|
126
|
+
)
|
|
127
|
+
return UNKNOWN_ENUM_VALUE
|
|
117
128
|
return normalized
|
|
118
129
|
|
|
119
130
|
def _apply_viewer_joined(self, viewer: ViewerInfo | None) -> None:
|
harbor/events.py
CHANGED
|
@@ -178,7 +178,7 @@ def parse_topic(
|
|
|
178
178
|
return None, None, None
|
|
179
179
|
|
|
180
180
|
root, serial, event_root, *rest = parts
|
|
181
|
-
if event_root
|
|
181
|
+
if event_root not in {"events", "responses"} or not rest:
|
|
182
182
|
return None, None, None
|
|
183
183
|
|
|
184
184
|
if root == "cameras":
|
|
@@ -336,7 +336,7 @@ def parse_message(
|
|
|
336
336
|
)
|
|
337
337
|
return RawEventUpdate(payload=raw_payload, **base_kwargs)
|
|
338
338
|
|
|
339
|
-
if event_key
|
|
339
|
+
if event_key in {"settings", "get_settings"}:
|
|
340
340
|
if typed_payload := _validate_payload(SettingsEvent, raw_payload, topic):
|
|
341
341
|
return SettingsUpdate(payload=typed_payload, **base_kwargs)
|
|
342
342
|
return RawEventUpdate(payload=raw_payload, **base_kwargs)
|
harbor/mqtt.py
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import asyncio
|
|
2
4
|
import json
|
|
3
5
|
import logging
|
|
4
6
|
import sys
|
|
5
7
|
from collections.abc import Awaitable, Callable
|
|
6
|
-
from typing import Any
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
from uuid import uuid4
|
|
7
10
|
|
|
8
11
|
from aiomqtt import Client, MqttError
|
|
9
12
|
|
|
10
13
|
from .config import HarborCameraConfig
|
|
14
|
+
from .data.mqtt_models import GetCameraSettingsRequest, SettingsEvent
|
|
11
15
|
from .utils import get_camera_host, get_ssl_cache_key, get_ssl_context
|
|
12
16
|
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from .events import HarborEvent
|
|
19
|
+
|
|
13
20
|
_LOGGER = logging.getLogger(__name__)
|
|
14
21
|
|
|
15
22
|
DEFAULT_CONNECTION_GRACE_PERIOD = 90.0
|
|
23
|
+
DEFAULT_COMMAND_QOS = 2
|
|
24
|
+
DEFAULT_REQUEST_TIMEOUT = 10.0
|
|
25
|
+
GET_SETTINGS_COMMAND = "get-settings"
|
|
26
|
+
DEFAULT_INITIAL_COMMANDS = (GET_SETTINGS_COMMAND,)
|
|
16
27
|
|
|
17
28
|
|
|
18
29
|
class HarborMQTTClient:
|
|
@@ -20,11 +31,12 @@ class HarborMQTTClient:
|
|
|
20
31
|
self,
|
|
21
32
|
config: HarborCameraConfig,
|
|
22
33
|
topics: list[str],
|
|
23
|
-
message_handler: Callable[[str, Any], Awaitable[None]],
|
|
34
|
+
message_handler: Callable[[str, Any], Awaitable[HarborEvent | None]],
|
|
24
35
|
client_id: str | None = None,
|
|
25
36
|
ssl_context_cache: dict | None = None,
|
|
26
37
|
on_connection_change: Callable[[bool], Awaitable[None]] | None = None,
|
|
27
38
|
connection_grace_period: float = DEFAULT_CONNECTION_GRACE_PERIOD,
|
|
39
|
+
initial_commands: list[str] | tuple[str, ...] | None = None,
|
|
28
40
|
) -> None:
|
|
29
41
|
"""Initialize the MQTT client.
|
|
30
42
|
|
|
@@ -42,9 +54,12 @@ class HarborMQTTClient:
|
|
|
42
54
|
self.ssl_context_cache = ssl_context_cache or {}
|
|
43
55
|
self.on_connection_change = on_connection_change
|
|
44
56
|
self.connection_grace_period = connection_grace_period
|
|
57
|
+
self.initial_commands = tuple(initial_commands or ())
|
|
45
58
|
self.connected: bool = False
|
|
46
59
|
self._stop_event = asyncio.Event()
|
|
47
60
|
self._task: asyncio.Task | None = None
|
|
61
|
+
self._client: Client | None = None
|
|
62
|
+
self._pending_responses: dict[str, asyncio.Future[Any]] = {}
|
|
48
63
|
self._reported_connected: bool | None = None
|
|
49
64
|
self._disconnect_grace_task: asyncio.Task | None = None
|
|
50
65
|
|
|
@@ -55,6 +70,23 @@ class HarborMQTTClient:
|
|
|
55
70
|
payload = payload_raw
|
|
56
71
|
|
|
57
72
|
await self.message_handler(topic, payload)
|
|
73
|
+
self._resolve_pending_response(topic, payload)
|
|
74
|
+
|
|
75
|
+
def _resolve_pending_response(self, topic: str, payload: Any) -> None:
|
|
76
|
+
"""Resolve a pending request when a camera response echoes its seq."""
|
|
77
|
+
if not topic.startswith(f"cameras/{self.config.serial}/responses/"):
|
|
78
|
+
return
|
|
79
|
+
if not isinstance(payload, dict):
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
seq = payload.get("seq")
|
|
83
|
+
if not isinstance(seq, str):
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
future = self._pending_responses.pop(seq, None)
|
|
87
|
+
if future is None or future.done():
|
|
88
|
+
return
|
|
89
|
+
future.set_result(payload)
|
|
58
90
|
|
|
59
91
|
async def _set_connected(self, connected: bool) -> None:
|
|
60
92
|
"""Update the raw connection flag and debounce listener notifications."""
|
|
@@ -108,6 +140,12 @@ class HarborMQTTClient:
|
|
|
108
140
|
def _invalidate_ssl_cache(self) -> None:
|
|
109
141
|
self.ssl_context_cache.pop(get_ssl_cache_key(self.config), None)
|
|
110
142
|
|
|
143
|
+
def _fail_pending_responses(self, exc: Exception) -> None:
|
|
144
|
+
for future in self._pending_responses.values():
|
|
145
|
+
if not future.done():
|
|
146
|
+
future.set_exception(exc)
|
|
147
|
+
self._pending_responses.clear()
|
|
148
|
+
|
|
111
149
|
async def run(self) -> None:
|
|
112
150
|
reconnect_delay = 2
|
|
113
151
|
|
|
@@ -143,6 +181,7 @@ class HarborMQTTClient:
|
|
|
143
181
|
timeout=10,
|
|
144
182
|
identifier=self.client_id,
|
|
145
183
|
) as client:
|
|
184
|
+
self._client = client
|
|
146
185
|
_LOGGER.info(
|
|
147
186
|
"Harbor: MQTT connected to %s:%s for camera %s",
|
|
148
187
|
host,
|
|
@@ -159,6 +198,8 @@ class HarborMQTTClient:
|
|
|
159
198
|
self.config.serial,
|
|
160
199
|
)
|
|
161
200
|
|
|
201
|
+
await self._publish_initial_commands()
|
|
202
|
+
|
|
162
203
|
async for message in client.messages:
|
|
163
204
|
if self._stop_event.is_set():
|
|
164
205
|
break
|
|
@@ -177,6 +218,8 @@ class HarborMQTTClient:
|
|
|
177
218
|
|
|
178
219
|
reconnect_delay = 2
|
|
179
220
|
finally:
|
|
221
|
+
self._client = None
|
|
222
|
+
self._fail_pending_responses(ConnectionError("Harbor MQTT client disconnected"))
|
|
180
223
|
await self._set_connected(False)
|
|
181
224
|
|
|
182
225
|
except TimeoutError as e:
|
|
@@ -247,9 +290,131 @@ class HarborMQTTClient:
|
|
|
247
290
|
_LOGGER.info("Harbor: MQTT client stopped for camera %s", self.config.serial)
|
|
248
291
|
# An intentional stop is a stable disconnect: skip the grace period.
|
|
249
292
|
self._cancel_disconnect_grace()
|
|
293
|
+
self._client = None
|
|
294
|
+
self._fail_pending_responses(ConnectionError("Harbor MQTT client stopped"))
|
|
250
295
|
if self._reported_connected:
|
|
251
296
|
await self._notify_connection_change(False)
|
|
252
297
|
|
|
298
|
+
async def publish(
|
|
299
|
+
self,
|
|
300
|
+
topic: str,
|
|
301
|
+
payload: Any,
|
|
302
|
+
*,
|
|
303
|
+
qos: int = DEFAULT_COMMAND_QOS,
|
|
304
|
+
retain: bool = False,
|
|
305
|
+
) -> None:
|
|
306
|
+
"""Publish a JSON-compatible payload to a Harbor MQTT topic."""
|
|
307
|
+
client = self._client
|
|
308
|
+
if client is None or not self.connected:
|
|
309
|
+
raise ConnectionError(f"Harbor MQTT client is not connected for camera {self.config.serial}")
|
|
310
|
+
|
|
311
|
+
if isinstance(payload, str):
|
|
312
|
+
payload_raw = payload
|
|
313
|
+
else:
|
|
314
|
+
payload_raw = json.dumps(payload, separators=(",", ":"))
|
|
315
|
+
|
|
316
|
+
_LOGGER.debug(
|
|
317
|
+
"Harbor: MQTT publishing to topic '%s' for camera %s: %s",
|
|
318
|
+
topic,
|
|
319
|
+
self.config.serial,
|
|
320
|
+
payload_raw,
|
|
321
|
+
)
|
|
322
|
+
await client.publish(topic, payload_raw, qos=qos, retain=retain)
|
|
323
|
+
|
|
324
|
+
async def publish_command(
|
|
325
|
+
self,
|
|
326
|
+
command: str,
|
|
327
|
+
payload: Any,
|
|
328
|
+
*,
|
|
329
|
+
qos: int = DEFAULT_COMMAND_QOS,
|
|
330
|
+
) -> None:
|
|
331
|
+
"""Publish a camera command using the app's command topic layout."""
|
|
332
|
+
await self.publish(f"cameras/{self.config.serial}/{command}", payload, qos=qos)
|
|
333
|
+
|
|
334
|
+
async def _publish_initial_commands(self) -> None:
|
|
335
|
+
"""Publish configured one-shot state population commands after connect."""
|
|
336
|
+
for command in self.initial_commands:
|
|
337
|
+
try:
|
|
338
|
+
if command == GET_SETTINGS_COMMAND:
|
|
339
|
+
await self.publish_command(command, self._build_get_settings_payload())
|
|
340
|
+
else:
|
|
341
|
+
_LOGGER.warning(
|
|
342
|
+
"Harbor: skipping unsupported initial command %s for camera %s",
|
|
343
|
+
command,
|
|
344
|
+
self.config.serial,
|
|
345
|
+
)
|
|
346
|
+
except Exception:
|
|
347
|
+
_LOGGER.exception(
|
|
348
|
+
"Harbor: failed to publish initial command %s for camera %s",
|
|
349
|
+
command,
|
|
350
|
+
self.config.serial,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
def _build_get_settings_payload(
|
|
354
|
+
self,
|
|
355
|
+
*,
|
|
356
|
+
client: str | None = None,
|
|
357
|
+
triggered_by: str | None = None,
|
|
358
|
+
seq: str | None = None,
|
|
359
|
+
) -> dict[str, Any]:
|
|
360
|
+
request = GetCameraSettingsRequest(
|
|
361
|
+
seq=seq or _generate_seq(),
|
|
362
|
+
client=client or self.client_id or f"harbor-client-{self.config.serial}",
|
|
363
|
+
triggered_by=triggered_by or "harbor-python",
|
|
364
|
+
)
|
|
365
|
+
return request.model_dump(by_alias=True)
|
|
366
|
+
|
|
367
|
+
async def request_command(
|
|
368
|
+
self,
|
|
369
|
+
command: str,
|
|
370
|
+
payload: dict[str, Any],
|
|
371
|
+
*,
|
|
372
|
+
seq: str | None = None,
|
|
373
|
+
timeout: float = DEFAULT_REQUEST_TIMEOUT,
|
|
374
|
+
qos: int = DEFAULT_COMMAND_QOS,
|
|
375
|
+
) -> Any:
|
|
376
|
+
"""Publish a command and wait for a response carrying the same seq."""
|
|
377
|
+
request_seq = seq or _generate_seq()
|
|
378
|
+
payload = {**payload, "seq": request_seq}
|
|
379
|
+
loop = asyncio.get_running_loop()
|
|
380
|
+
future: asyncio.Future[Any] = loop.create_future()
|
|
381
|
+
self._pending_responses[request_seq] = future
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
await self.publish_command(command, payload, qos=qos)
|
|
385
|
+
return await asyncio.wait_for(future, timeout=timeout)
|
|
386
|
+
except Exception:
|
|
387
|
+
pending = self._pending_responses.pop(request_seq, None)
|
|
388
|
+
if pending is not None and not pending.done():
|
|
389
|
+
pending.cancel()
|
|
390
|
+
raise
|
|
391
|
+
|
|
392
|
+
async def get_settings(
|
|
393
|
+
self,
|
|
394
|
+
*,
|
|
395
|
+
client: str | None = None,
|
|
396
|
+
triggered_by: str | None = None,
|
|
397
|
+
timeout: float = DEFAULT_REQUEST_TIMEOUT,
|
|
398
|
+
) -> SettingsEvent:
|
|
399
|
+
"""Request camera settings via the get-settings command."""
|
|
400
|
+
seq = _generate_seq()
|
|
401
|
+
response = await self.request_command(
|
|
402
|
+
GET_SETTINGS_COMMAND,
|
|
403
|
+
self._build_get_settings_payload(
|
|
404
|
+
seq=seq,
|
|
405
|
+
client=client,
|
|
406
|
+
triggered_by=triggered_by,
|
|
407
|
+
),
|
|
408
|
+
seq=seq,
|
|
409
|
+
timeout=timeout,
|
|
410
|
+
)
|
|
411
|
+
return SettingsEvent.model_validate(response)
|
|
412
|
+
|
|
253
413
|
def __del__(self) -> None:
|
|
254
414
|
if self._stop_event and not self._stop_event.is_set():
|
|
255
415
|
self._stop_event.set()
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _generate_seq() -> str:
|
|
419
|
+
"""Generate a request sequence string echoed by Harbor responses."""
|
|
420
|
+
return uuid4().hex
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
harbor/__init__.py,sha256=F2Yo9T1Hpf0lSioVAk-m8GJnEh1tLd2En4Mzz9RxczY,1606
|
|
2
|
+
harbor/config.py,sha256=LXMDaTC_0Z9wF_vTXJG9C0vQ0XHXo4EuFL0-Kwa4-S4,1373
|
|
3
|
+
harbor/core.py,sha256=Alm6ojRBCibsQ5xGGyaAOQimi-_kE6Q6fuS2ZAxzaNM,3468
|
|
4
|
+
harbor/device.py,sha256=YvaFWieXJ-EzQJU8VQo_ZERl65CynITTAnMFto-PF_I,5933
|
|
5
|
+
harbor/events.py,sha256=4Y3JT_NP1x8rUR7U6n2-LvAENt9zpqD4jVy28AAYo4Q,20400
|
|
6
|
+
harbor/mqtt.py,sha256=43QzJrocLGBWNad7uH4mSm2M9YR15xtmSTvIviwrkXA,16832
|
|
7
|
+
harbor/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
harbor/state.py,sha256=jQDLV8zpkLV2f6zR4LuOtk_B5BtGdp-8dq4NyGZCa-8,1210
|
|
9
|
+
harbor/utils.py,sha256=7A3Tn1aZ93SlxRKY-0b-50ntQL4Pw1j5bXwkJvnarpY,3312
|
|
10
|
+
harbor/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
harbor/data/mqtt_models.py,sha256=OF36ZoUCOeoaKp4TPggM6HCbowgtkH1zLFTozVJUZYY,4314
|
|
12
|
+
harbor/devices/camera.py,sha256=ciq8ynsAaZgW0CWYjKiJGAZwk04pNz4uYfnsN0GT0O4,8429
|
|
13
|
+
harbor/devices/monitor.py,sha256=hzx4lu92h3i_34QyYK392gE7CihqERiOZuLhyzDtDk0,452
|
|
14
|
+
harbor_python-1.2.1.dist-info/METADATA,sha256=NOeg3ZjAJU9Ji4zRwKXA_ouXAdHnHcn3m9UVNpx9ixQ,688
|
|
15
|
+
harbor_python-1.2.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
16
|
+
harbor_python-1.2.1.dist-info/licenses/LICENSE,sha256=QEOXEfrOQetemKy4k1WN5XCApJdpiRtyV5jLMSToVVw,11344
|
|
17
|
+
harbor_python-1.2.1.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
harbor/__init__.py,sha256=b43EC904n7dBigKTlU8600P5XxG4Zip0It4I7GVsRDc,1544
|
|
2
|
-
harbor/config.py,sha256=LXMDaTC_0Z9wF_vTXJG9C0vQ0XHXo4EuFL0-Kwa4-S4,1373
|
|
3
|
-
harbor/core.py,sha256=lfO1FAwYKSSW7YZ8fqDdhgpWXS6eA-vL1NgZKmN_H_g,2186
|
|
4
|
-
harbor/device.py,sha256=YvaFWieXJ-EzQJU8VQo_ZERl65CynITTAnMFto-PF_I,5933
|
|
5
|
-
harbor/events.py,sha256=q_WNG_yIkCczOzloe9mDwulgHhXmeQdZFDyyKpCz1Ig,20363
|
|
6
|
-
harbor/mqtt.py,sha256=ZqHVjHaocXtOK6wh0FO6lLF2UJQfDX3W6DG8R9YD30c,10816
|
|
7
|
-
harbor/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
harbor/state.py,sha256=jQDLV8zpkLV2f6zR4LuOtk_B5BtGdp-8dq4NyGZCa-8,1210
|
|
9
|
-
harbor/utils.py,sha256=7A3Tn1aZ93SlxRKY-0b-50ntQL4Pw1j5bXwkJvnarpY,3312
|
|
10
|
-
harbor/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
harbor/data/mqtt_models.py,sha256=yir1maFUxmk_9E2J_JEnTzDJwiAGDdSrbkGN1YaZd_o,4031
|
|
12
|
-
harbor/devices/camera.py,sha256=jjgXFeASsSnQqyGTmhMbeLZJ3zHrdxLAKXG2UwHYWSk,7968
|
|
13
|
-
harbor/devices/monitor.py,sha256=hzx4lu92h3i_34QyYK392gE7CihqERiOZuLhyzDtDk0,452
|
|
14
|
-
harbor_python-1.1.0.dist-info/METADATA,sha256=FodUxQmoYkqajgqYsSxXzrLxHm9W_pdBnXkSs5eIs5s,688
|
|
15
|
-
harbor_python-1.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
16
|
-
harbor_python-1.1.0.dist-info/licenses/LICENSE,sha256=QEOXEfrOQetemKy4k1WN5XCApJdpiRtyV5jLMSToVVw,11344
|
|
17
|
-
harbor_python-1.1.0.dist-info/RECORD,,
|
|
File without changes
|