harbor-python 1.1.0__py3-none-any.whl → 1.2.0__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 +160 -0
- {harbor_python-1.1.0.dist-info → harbor_python-1.2.0.dist-info}/METADATA +1 -1
- harbor_python-1.2.0.dist-info/RECORD +17 -0
- {harbor_python-1.1.0.dist-info → harbor_python-1.2.0.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.0.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
|
@@ -4,15 +4,21 @@ import logging
|
|
|
4
4
|
import sys
|
|
5
5
|
from collections.abc import Awaitable, Callable
|
|
6
6
|
from typing import Any
|
|
7
|
+
from uuid import uuid4
|
|
7
8
|
|
|
8
9
|
from aiomqtt import Client, MqttError
|
|
9
10
|
|
|
10
11
|
from .config import HarborCameraConfig
|
|
12
|
+
from .data.mqtt_models import GetCameraSettingsRequest, SettingsEvent
|
|
11
13
|
from .utils import get_camera_host, get_ssl_cache_key, get_ssl_context
|
|
12
14
|
|
|
13
15
|
_LOGGER = logging.getLogger(__name__)
|
|
14
16
|
|
|
15
17
|
DEFAULT_CONNECTION_GRACE_PERIOD = 90.0
|
|
18
|
+
DEFAULT_COMMAND_QOS = 2
|
|
19
|
+
DEFAULT_REQUEST_TIMEOUT = 10.0
|
|
20
|
+
GET_SETTINGS_COMMAND = "get-settings"
|
|
21
|
+
DEFAULT_INITIAL_COMMANDS = (GET_SETTINGS_COMMAND,)
|
|
16
22
|
|
|
17
23
|
|
|
18
24
|
class HarborMQTTClient:
|
|
@@ -25,6 +31,7 @@ class HarborMQTTClient:
|
|
|
25
31
|
ssl_context_cache: dict | None = None,
|
|
26
32
|
on_connection_change: Callable[[bool], Awaitable[None]] | None = None,
|
|
27
33
|
connection_grace_period: float = DEFAULT_CONNECTION_GRACE_PERIOD,
|
|
34
|
+
initial_commands: list[str] | tuple[str, ...] | None = None,
|
|
28
35
|
) -> None:
|
|
29
36
|
"""Initialize the MQTT client.
|
|
30
37
|
|
|
@@ -42,9 +49,12 @@ class HarborMQTTClient:
|
|
|
42
49
|
self.ssl_context_cache = ssl_context_cache or {}
|
|
43
50
|
self.on_connection_change = on_connection_change
|
|
44
51
|
self.connection_grace_period = connection_grace_period
|
|
52
|
+
self.initial_commands = tuple(initial_commands or ())
|
|
45
53
|
self.connected: bool = False
|
|
46
54
|
self._stop_event = asyncio.Event()
|
|
47
55
|
self._task: asyncio.Task | None = None
|
|
56
|
+
self._client: Client | None = None
|
|
57
|
+
self._pending_responses: dict[str, asyncio.Future[Any]] = {}
|
|
48
58
|
self._reported_connected: bool | None = None
|
|
49
59
|
self._disconnect_grace_task: asyncio.Task | None = None
|
|
50
60
|
|
|
@@ -55,6 +65,23 @@ class HarborMQTTClient:
|
|
|
55
65
|
payload = payload_raw
|
|
56
66
|
|
|
57
67
|
await self.message_handler(topic, payload)
|
|
68
|
+
self._resolve_pending_response(topic, payload)
|
|
69
|
+
|
|
70
|
+
def _resolve_pending_response(self, topic: str, payload: Any) -> None:
|
|
71
|
+
"""Resolve a pending request when a camera response echoes its seq."""
|
|
72
|
+
if not topic.startswith(f"cameras/{self.config.serial}/responses/"):
|
|
73
|
+
return
|
|
74
|
+
if not isinstance(payload, dict):
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
seq = payload.get("seq")
|
|
78
|
+
if not isinstance(seq, str):
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
future = self._pending_responses.pop(seq, None)
|
|
82
|
+
if future is None or future.done():
|
|
83
|
+
return
|
|
84
|
+
future.set_result(payload)
|
|
58
85
|
|
|
59
86
|
async def _set_connected(self, connected: bool) -> None:
|
|
60
87
|
"""Update the raw connection flag and debounce listener notifications."""
|
|
@@ -108,6 +135,12 @@ class HarborMQTTClient:
|
|
|
108
135
|
def _invalidate_ssl_cache(self) -> None:
|
|
109
136
|
self.ssl_context_cache.pop(get_ssl_cache_key(self.config), None)
|
|
110
137
|
|
|
138
|
+
def _fail_pending_responses(self, exc: Exception) -> None:
|
|
139
|
+
for future in self._pending_responses.values():
|
|
140
|
+
if not future.done():
|
|
141
|
+
future.set_exception(exc)
|
|
142
|
+
self._pending_responses.clear()
|
|
143
|
+
|
|
111
144
|
async def run(self) -> None:
|
|
112
145
|
reconnect_delay = 2
|
|
113
146
|
|
|
@@ -143,6 +176,7 @@ class HarborMQTTClient:
|
|
|
143
176
|
timeout=10,
|
|
144
177
|
identifier=self.client_id,
|
|
145
178
|
) as client:
|
|
179
|
+
self._client = client
|
|
146
180
|
_LOGGER.info(
|
|
147
181
|
"Harbor: MQTT connected to %s:%s for camera %s",
|
|
148
182
|
host,
|
|
@@ -159,6 +193,8 @@ class HarborMQTTClient:
|
|
|
159
193
|
self.config.serial,
|
|
160
194
|
)
|
|
161
195
|
|
|
196
|
+
await self._publish_initial_commands()
|
|
197
|
+
|
|
162
198
|
async for message in client.messages:
|
|
163
199
|
if self._stop_event.is_set():
|
|
164
200
|
break
|
|
@@ -177,6 +213,8 @@ class HarborMQTTClient:
|
|
|
177
213
|
|
|
178
214
|
reconnect_delay = 2
|
|
179
215
|
finally:
|
|
216
|
+
self._client = None
|
|
217
|
+
self._fail_pending_responses(ConnectionError("Harbor MQTT client disconnected"))
|
|
180
218
|
await self._set_connected(False)
|
|
181
219
|
|
|
182
220
|
except TimeoutError as e:
|
|
@@ -247,9 +285,131 @@ class HarborMQTTClient:
|
|
|
247
285
|
_LOGGER.info("Harbor: MQTT client stopped for camera %s", self.config.serial)
|
|
248
286
|
# An intentional stop is a stable disconnect: skip the grace period.
|
|
249
287
|
self._cancel_disconnect_grace()
|
|
288
|
+
self._client = None
|
|
289
|
+
self._fail_pending_responses(ConnectionError("Harbor MQTT client stopped"))
|
|
250
290
|
if self._reported_connected:
|
|
251
291
|
await self._notify_connection_change(False)
|
|
252
292
|
|
|
293
|
+
async def publish(
|
|
294
|
+
self,
|
|
295
|
+
topic: str,
|
|
296
|
+
payload: Any,
|
|
297
|
+
*,
|
|
298
|
+
qos: int = DEFAULT_COMMAND_QOS,
|
|
299
|
+
retain: bool = False,
|
|
300
|
+
) -> None:
|
|
301
|
+
"""Publish a JSON-compatible payload to a Harbor MQTT topic."""
|
|
302
|
+
client = self._client
|
|
303
|
+
if client is None or not self.connected:
|
|
304
|
+
raise ConnectionError(f"Harbor MQTT client is not connected for camera {self.config.serial}")
|
|
305
|
+
|
|
306
|
+
if isinstance(payload, str):
|
|
307
|
+
payload_raw = payload
|
|
308
|
+
else:
|
|
309
|
+
payload_raw = json.dumps(payload, separators=(",", ":"))
|
|
310
|
+
|
|
311
|
+
_LOGGER.debug(
|
|
312
|
+
"Harbor: MQTT publishing to topic '%s' for camera %s: %s",
|
|
313
|
+
topic,
|
|
314
|
+
self.config.serial,
|
|
315
|
+
payload_raw,
|
|
316
|
+
)
|
|
317
|
+
await client.publish(topic, payload_raw, qos=qos, retain=retain)
|
|
318
|
+
|
|
319
|
+
async def publish_command(
|
|
320
|
+
self,
|
|
321
|
+
command: str,
|
|
322
|
+
payload: Any,
|
|
323
|
+
*,
|
|
324
|
+
qos: int = DEFAULT_COMMAND_QOS,
|
|
325
|
+
) -> None:
|
|
326
|
+
"""Publish a camera command using the app's command topic layout."""
|
|
327
|
+
await self.publish(f"cameras/{self.config.serial}/{command}", payload, qos=qos)
|
|
328
|
+
|
|
329
|
+
async def _publish_initial_commands(self) -> None:
|
|
330
|
+
"""Publish configured one-shot state population commands after connect."""
|
|
331
|
+
for command in self.initial_commands:
|
|
332
|
+
try:
|
|
333
|
+
if command == GET_SETTINGS_COMMAND:
|
|
334
|
+
await self.publish_command(command, self._build_get_settings_payload())
|
|
335
|
+
else:
|
|
336
|
+
_LOGGER.warning(
|
|
337
|
+
"Harbor: skipping unsupported initial command %s for camera %s",
|
|
338
|
+
command,
|
|
339
|
+
self.config.serial,
|
|
340
|
+
)
|
|
341
|
+
except Exception:
|
|
342
|
+
_LOGGER.exception(
|
|
343
|
+
"Harbor: failed to publish initial command %s for camera %s",
|
|
344
|
+
command,
|
|
345
|
+
self.config.serial,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
def _build_get_settings_payload(
|
|
349
|
+
self,
|
|
350
|
+
*,
|
|
351
|
+
client: str | None = None,
|
|
352
|
+
triggered_by: str | None = None,
|
|
353
|
+
seq: str | None = None,
|
|
354
|
+
) -> dict[str, Any]:
|
|
355
|
+
request = GetCameraSettingsRequest(
|
|
356
|
+
seq=seq or _generate_seq(),
|
|
357
|
+
client=client or self.client_id or f"harbor-client-{self.config.serial}",
|
|
358
|
+
triggered_by=triggered_by or "harbor-python",
|
|
359
|
+
)
|
|
360
|
+
return request.model_dump(by_alias=True)
|
|
361
|
+
|
|
362
|
+
async def request_command(
|
|
363
|
+
self,
|
|
364
|
+
command: str,
|
|
365
|
+
payload: dict[str, Any],
|
|
366
|
+
*,
|
|
367
|
+
seq: str | None = None,
|
|
368
|
+
timeout: float = DEFAULT_REQUEST_TIMEOUT,
|
|
369
|
+
qos: int = DEFAULT_COMMAND_QOS,
|
|
370
|
+
) -> Any:
|
|
371
|
+
"""Publish a command and wait for a response carrying the same seq."""
|
|
372
|
+
request_seq = seq or _generate_seq()
|
|
373
|
+
payload = {**payload, "seq": request_seq}
|
|
374
|
+
loop = asyncio.get_running_loop()
|
|
375
|
+
future: asyncio.Future[Any] = loop.create_future()
|
|
376
|
+
self._pending_responses[request_seq] = future
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
await self.publish_command(command, payload, qos=qos)
|
|
380
|
+
return await asyncio.wait_for(future, timeout=timeout)
|
|
381
|
+
except Exception:
|
|
382
|
+
pending = self._pending_responses.pop(request_seq, None)
|
|
383
|
+
if pending is not None and not pending.done():
|
|
384
|
+
pending.cancel()
|
|
385
|
+
raise
|
|
386
|
+
|
|
387
|
+
async def get_settings(
|
|
388
|
+
self,
|
|
389
|
+
*,
|
|
390
|
+
client: str | None = None,
|
|
391
|
+
triggered_by: str | None = None,
|
|
392
|
+
timeout: float = DEFAULT_REQUEST_TIMEOUT,
|
|
393
|
+
) -> SettingsEvent:
|
|
394
|
+
"""Request camera settings via the get-settings command."""
|
|
395
|
+
seq = _generate_seq()
|
|
396
|
+
response = await self.request_command(
|
|
397
|
+
GET_SETTINGS_COMMAND,
|
|
398
|
+
self._build_get_settings_payload(
|
|
399
|
+
seq=seq,
|
|
400
|
+
client=client,
|
|
401
|
+
triggered_by=triggered_by,
|
|
402
|
+
),
|
|
403
|
+
seq=seq,
|
|
404
|
+
timeout=timeout,
|
|
405
|
+
)
|
|
406
|
+
return SettingsEvent.model_validate(response)
|
|
407
|
+
|
|
253
408
|
def __del__(self) -> None:
|
|
254
409
|
if self._stop_event and not self._stop_event.is_set():
|
|
255
410
|
self._stop_event.set()
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _generate_seq() -> str:
|
|
414
|
+
"""Generate a request sequence string echoed by Harbor responses."""
|
|
415
|
+
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=ZhGuR90U449q4OY9WCCJVLIuYVEHrG1uT32EGjux7rU,16712
|
|
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.0.dist-info/METADATA,sha256=ZEPjl-fqvicWCUsIa5dbS18zAFgm5YeYXbGUqvMFe9A,688
|
|
15
|
+
harbor_python-1.2.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
16
|
+
harbor_python-1.2.0.dist-info/licenses/LICENSE,sha256=QEOXEfrOQetemKy4k1WN5XCApJdpiRtyV5jLMSToVVw,11344
|
|
17
|
+
harbor_python-1.2.0.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
|