harbor-python 1.0.0__tar.gz → 1.1.0__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.
- {harbor_python-1.0.0 → harbor_python-1.1.0}/PKG-INFO +1 -1
- {harbor_python-1.0.0 → harbor_python-1.1.0}/harbor/__init__.py +3 -1
- harbor_python-1.1.0/harbor/config.py +34 -0
- {harbor_python-1.0.0 → harbor_python-1.1.0}/harbor/devices/camera.py +41 -2
- {harbor_python-1.0.0 → harbor_python-1.1.0}/harbor/mqtt.py +74 -20
- harbor_python-1.1.0/harbor/utils.py +82 -0
- {harbor_python-1.0.0 → harbor_python-1.1.0}/pyproject.toml +1 -1
- harbor_python-1.0.0/harbor/config.py +0 -11
- harbor_python-1.0.0/harbor/utils.py +0 -39
- {harbor_python-1.0.0 → harbor_python-1.1.0}/.gitignore +0 -0
- {harbor_python-1.0.0 → harbor_python-1.1.0}/LICENSE +0 -0
- {harbor_python-1.0.0 → harbor_python-1.1.0}/README.md +0 -0
- {harbor_python-1.0.0 → harbor_python-1.1.0}/harbor/core.py +0 -0
- {harbor_python-1.0.0 → harbor_python-1.1.0}/harbor/data/__init__.py +0 -0
- {harbor_python-1.0.0 → harbor_python-1.1.0}/harbor/data/mqtt_models.py +0 -0
- {harbor_python-1.0.0 → harbor_python-1.1.0}/harbor/device.py +0 -0
- {harbor_python-1.0.0 → harbor_python-1.1.0}/harbor/devices/monitor.py +0 -0
- {harbor_python-1.0.0 → harbor_python-1.1.0}/harbor/events.py +0 -0
- {harbor_python-1.0.0 → harbor_python-1.1.0}/harbor/py.typed +0 -0
- {harbor_python-1.0.0 → harbor_python-1.1.0}/harbor/state.py +0 -0
|
@@ -9,7 +9,7 @@ from .data.mqtt_models import (
|
|
|
9
9
|
ViewerLeftEvent,
|
|
10
10
|
)
|
|
11
11
|
from .device import HarborDevice
|
|
12
|
-
from .devices.camera import HarborCamera
|
|
12
|
+
from .devices.camera import SPEAKER_STATES, STREAM_QUALITIES, HarborCamera
|
|
13
13
|
from .devices.monitor import HarborMonitor
|
|
14
14
|
from .events import (
|
|
15
15
|
CameraEventUpdate,
|
|
@@ -59,4 +59,6 @@ __all__ = [
|
|
|
59
59
|
"HarborViewer",
|
|
60
60
|
"HarborEventState",
|
|
61
61
|
"HarborDeviceState",
|
|
62
|
+
"SPEAKER_STATES",
|
|
63
|
+
"STREAM_QUALITIES",
|
|
62
64
|
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass(frozen=True)
|
|
5
|
+
class HarborCameraConfig:
|
|
6
|
+
"""Connection configuration for a Harbor camera.
|
|
7
|
+
|
|
8
|
+
Client certificate material can be provided either as in-memory PEM
|
|
9
|
+
strings (``cert_pem``/``key_pem``) or as file paths
|
|
10
|
+
(``cert_path``/``key_path``). PEM strings are preferred for consumers
|
|
11
|
+
that should not manage certificate files themselves: while building the
|
|
12
|
+
SSL context the library briefly stages the PEM data in a private
|
|
13
|
+
temporary directory (0600 files) that is deleted before the build
|
|
14
|
+
returns, so the caller never handles files. When both are provided, the
|
|
15
|
+
PEM strings win.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
serial: str
|
|
19
|
+
cert_path: str | None = None
|
|
20
|
+
key_path: str | None = None
|
|
21
|
+
cert_dir: str | None = None
|
|
22
|
+
|
|
23
|
+
ip_address: str | None = None
|
|
24
|
+
|
|
25
|
+
cert_pem: str | None = None
|
|
26
|
+
key_pem: str | None = None
|
|
27
|
+
|
|
28
|
+
def __post_init__(self) -> None:
|
|
29
|
+
if (self.cert_pem is None) != (self.key_pem is None):
|
|
30
|
+
raise ValueError("cert_pem and key_pem must be provided together")
|
|
31
|
+
if (self.cert_path is None) != (self.key_path is None):
|
|
32
|
+
raise ValueError("cert_path and key_path must be provided together")
|
|
33
|
+
if self.cert_pem is None and self.cert_path is None:
|
|
34
|
+
raise ValueError("Certificate material is required: provide cert_pem/key_pem or cert_path/key_path")
|
|
@@ -20,6 +20,15 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
20
20
|
DEFAULT_CAMERA_EVENT_KEYS = ("motion_detection", "cry_detection", "noise_detection")
|
|
21
21
|
DEFAULT_EVENT_ACTIVE_SECONDS = 5.0
|
|
22
22
|
|
|
23
|
+
# Known values for enumerated state fields. The device reports these in
|
|
24
|
+
# mixed/upper case (e.g. "PLAYING", "GOOD"); they are normalized to the
|
|
25
|
+
# lowercase values below before being stored in ``HarborDeviceState.values``.
|
|
26
|
+
# A value outside these sets is still stored lowercased (and logged once),
|
|
27
|
+
# so consumers that declare options up front should treat these sets as the
|
|
28
|
+
# baseline, not a hard guarantee.
|
|
29
|
+
SPEAKER_STATES = frozenset({"idle", "muted", "off", "paused", "playing", "unknown"})
|
|
30
|
+
STREAM_QUALITIES = frozenset({"excellent", "fair", "good", "poor", "unknown"})
|
|
31
|
+
|
|
23
32
|
|
|
24
33
|
class HarborCamera(HarborDevice):
|
|
25
34
|
"""Represents a Harbor camera device."""
|
|
@@ -29,6 +38,7 @@ class HarborCamera(HarborDevice):
|
|
|
29
38
|
super().__init__(config.serial, "camera")
|
|
30
39
|
self.config = config
|
|
31
40
|
self._event_reset_handles: dict[str, asyncio.TimerHandle] = {}
|
|
41
|
+
self._unexpected_enum_values: set[tuple[str, str]] = set()
|
|
32
42
|
|
|
33
43
|
for event_key in DEFAULT_CAMERA_EVENT_KEYS:
|
|
34
44
|
self._ensure_camera_event(event_key)
|
|
@@ -60,8 +70,14 @@ class HarborCamera(HarborDevice):
|
|
|
60
70
|
self._set_state_value("bitrate", payload.bitrate)
|
|
61
71
|
self._set_state_value("wifi_strength", payload.network_bars)
|
|
62
72
|
self._set_state_value("camera_present", payload.camera_present)
|
|
63
|
-
self._set_state_value(
|
|
64
|
-
|
|
73
|
+
self._set_state_value(
|
|
74
|
+
"speaker_state",
|
|
75
|
+
self._normalize_enum_value("speaker_state", payload.speaker_state, SPEAKER_STATES),
|
|
76
|
+
)
|
|
77
|
+
self._set_state_value(
|
|
78
|
+
"stream_quality",
|
|
79
|
+
self._normalize_enum_value("stream_quality", payload.stream_quality, STREAM_QUALITIES),
|
|
80
|
+
)
|
|
65
81
|
self._set_state_value("app_start_time", payload.app_start_time)
|
|
66
82
|
self._set_state_value("stream_start_time", payload.stream_start_time)
|
|
67
83
|
|
|
@@ -77,6 +93,29 @@ class HarborCamera(HarborDevice):
|
|
|
77
93
|
}
|
|
78
94
|
self.state.values["num_viewers"] = len(self.state.viewers)
|
|
79
95
|
|
|
96
|
+
def _normalize_enum_value(
|
|
97
|
+
self,
|
|
98
|
+
field_name: str,
|
|
99
|
+
value: str | None,
|
|
100
|
+
known_values: frozenset[str],
|
|
101
|
+
) -> str | None:
|
|
102
|
+
"""Normalize an enumerated device value to a stable lowercase string."""
|
|
103
|
+
if value is None:
|
|
104
|
+
return None
|
|
105
|
+
normalized = value.strip().lower()
|
|
106
|
+
if not normalized:
|
|
107
|
+
return None
|
|
108
|
+
if normalized not in known_values and (field_name, normalized) not in self._unexpected_enum_values:
|
|
109
|
+
self._unexpected_enum_values.add((field_name, normalized))
|
|
110
|
+
_LOGGER.warning(
|
|
111
|
+
"Camera %s reported unexpected %s value %r (known values: %s)",
|
|
112
|
+
self.serial,
|
|
113
|
+
field_name,
|
|
114
|
+
normalized,
|
|
115
|
+
sorted(known_values),
|
|
116
|
+
)
|
|
117
|
+
return normalized
|
|
118
|
+
|
|
80
119
|
def _apply_viewer_joined(self, viewer: ViewerInfo | None) -> None:
|
|
81
120
|
"""Apply a viewer joined update."""
|
|
82
121
|
if viewer is None:
|
|
@@ -8,10 +8,12 @@ from typing import Any
|
|
|
8
8
|
from aiomqtt import Client, MqttError
|
|
9
9
|
|
|
10
10
|
from .config import HarborCameraConfig
|
|
11
|
-
from .utils import get_camera_host, get_ssl_context
|
|
11
|
+
from .utils import get_camera_host, get_ssl_cache_key, get_ssl_context
|
|
12
12
|
|
|
13
13
|
_LOGGER = logging.getLogger(__name__)
|
|
14
14
|
|
|
15
|
+
DEFAULT_CONNECTION_GRACE_PERIOD = 90.0
|
|
16
|
+
|
|
15
17
|
|
|
16
18
|
class HarborMQTTClient:
|
|
17
19
|
def __init__(
|
|
@@ -22,16 +24,29 @@ class HarborMQTTClient:
|
|
|
22
24
|
client_id: str | None = None,
|
|
23
25
|
ssl_context_cache: dict | None = None,
|
|
24
26
|
on_connection_change: Callable[[bool], Awaitable[None]] | None = None,
|
|
27
|
+
connection_grace_period: float = DEFAULT_CONNECTION_GRACE_PERIOD,
|
|
25
28
|
) -> None:
|
|
29
|
+
"""Initialize the MQTT client.
|
|
30
|
+
|
|
31
|
+
``on_connection_change`` fires only on stable connection-state
|
|
32
|
+
transitions: a disconnect is reported only if the client stays
|
|
33
|
+
disconnected for ``connection_grace_period`` seconds, so routine
|
|
34
|
+
TCP flapping never reaches the listener. Set the grace period to 0
|
|
35
|
+
to report every raw transition. ``connected`` always reflects the
|
|
36
|
+
raw transport state.
|
|
37
|
+
"""
|
|
26
38
|
self.config = config
|
|
27
39
|
self.topics = topics
|
|
28
40
|
self.message_handler = message_handler
|
|
29
41
|
self.client_id = client_id
|
|
30
42
|
self.ssl_context_cache = ssl_context_cache or {}
|
|
31
43
|
self.on_connection_change = on_connection_change
|
|
44
|
+
self.connection_grace_period = connection_grace_period
|
|
32
45
|
self.connected: bool = False
|
|
33
46
|
self._stop_event = asyncio.Event()
|
|
34
47
|
self._task: asyncio.Task | None = None
|
|
48
|
+
self._reported_connected: bool | None = None
|
|
49
|
+
self._disconnect_grace_task: asyncio.Task | None = None
|
|
35
50
|
|
|
36
51
|
async def _handle_message(self, topic: str, payload_raw: str) -> None:
|
|
37
52
|
try:
|
|
@@ -42,10 +57,44 @@ class HarborMQTTClient:
|
|
|
42
57
|
await self.message_handler(topic, payload)
|
|
43
58
|
|
|
44
59
|
async def _set_connected(self, connected: bool) -> None:
|
|
45
|
-
"""Update the connection flag and
|
|
60
|
+
"""Update the raw connection flag and debounce listener notifications."""
|
|
46
61
|
if self.connected == connected:
|
|
47
62
|
return
|
|
48
63
|
self.connected = connected
|
|
64
|
+
|
|
65
|
+
if connected:
|
|
66
|
+
self._cancel_disconnect_grace()
|
|
67
|
+
if self._reported_connected is not True:
|
|
68
|
+
await self._notify_connection_change(True)
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
if self._reported_connected is not True:
|
|
72
|
+
return
|
|
73
|
+
if self.connection_grace_period <= 0:
|
|
74
|
+
await self._notify_connection_change(False)
|
|
75
|
+
return
|
|
76
|
+
if self._disconnect_grace_task is None or self._disconnect_grace_task.done():
|
|
77
|
+
self._disconnect_grace_task = asyncio.create_task(self._disconnect_after_grace())
|
|
78
|
+
|
|
79
|
+
async def _disconnect_after_grace(self) -> None:
|
|
80
|
+
"""Report a disconnect only if it survives the grace period."""
|
|
81
|
+
await asyncio.sleep(self.connection_grace_period)
|
|
82
|
+
self._disconnect_grace_task = None
|
|
83
|
+
if not self.connected:
|
|
84
|
+
_LOGGER.info(
|
|
85
|
+
"Harbor: camera %s still disconnected after %s second grace period",
|
|
86
|
+
self.config.serial,
|
|
87
|
+
self.connection_grace_period,
|
|
88
|
+
)
|
|
89
|
+
await self._notify_connection_change(False)
|
|
90
|
+
|
|
91
|
+
def _cancel_disconnect_grace(self) -> None:
|
|
92
|
+
if self._disconnect_grace_task is not None:
|
|
93
|
+
self._disconnect_grace_task.cancel()
|
|
94
|
+
self._disconnect_grace_task = None
|
|
95
|
+
|
|
96
|
+
async def _notify_connection_change(self, connected: bool) -> None:
|
|
97
|
+
self._reported_connected = connected
|
|
49
98
|
if self.on_connection_change is None:
|
|
50
99
|
return
|
|
51
100
|
try:
|
|
@@ -56,23 +105,28 @@ class HarborMQTTClient:
|
|
|
56
105
|
self.config.serial,
|
|
57
106
|
)
|
|
58
107
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
loop = asyncio.get_running_loop()
|
|
62
|
-
ssl_ctx = await loop.run_in_executor(None, get_ssl_context, self.config, self.ssl_context_cache)
|
|
63
|
-
except Exception as e:
|
|
64
|
-
_LOGGER.error("Harbor: Failed to create SSL context for camera %s: %s", self.config.serial, e)
|
|
65
|
-
# Ensure we clear any partial state
|
|
66
|
-
if self.config.serial in self.ssl_context_cache:
|
|
67
|
-
del self.ssl_context_cache[self.config.serial]
|
|
68
|
-
return
|
|
108
|
+
def _invalidate_ssl_cache(self) -> None:
|
|
109
|
+
self.ssl_context_cache.pop(get_ssl_cache_key(self.config), None)
|
|
69
110
|
|
|
111
|
+
async def run(self) -> None:
|
|
70
112
|
reconnect_delay = 2
|
|
71
113
|
|
|
72
114
|
_LOGGER.info("Harbor: MQTT client starting for camera %s", self.config.serial)
|
|
73
115
|
|
|
74
116
|
try:
|
|
75
117
|
while not self._stop_event.is_set():
|
|
118
|
+
# Fetch the SSL context each attempt so invalidations in the
|
|
119
|
+
# error handlers below take effect on the next reconnect;
|
|
120
|
+
# unchanged material is a cheap cache hit.
|
|
121
|
+
try:
|
|
122
|
+
loop = asyncio.get_running_loop()
|
|
123
|
+
ssl_ctx = await loop.run_in_executor(None, get_ssl_context, self.config, self.ssl_context_cache)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
_LOGGER.error("Harbor: Failed to create SSL context for camera %s: %s", self.config.serial, e)
|
|
126
|
+
# Ensure we clear any partial state
|
|
127
|
+
self._invalidate_ssl_cache()
|
|
128
|
+
return
|
|
129
|
+
|
|
76
130
|
try:
|
|
77
131
|
host = get_camera_host(self.config)
|
|
78
132
|
_LOGGER.info(
|
|
@@ -128,28 +182,24 @@ class HarborMQTTClient:
|
|
|
128
182
|
except TimeoutError as e:
|
|
129
183
|
_LOGGER.warning("Harbor: MQTT connection timeout for %s: %s (reconnecting)", self.config.serial, e)
|
|
130
184
|
# Clear SSL context on timeout as it might be a stale session
|
|
131
|
-
|
|
132
|
-
del self.ssl_context_cache[self.config.serial]
|
|
185
|
+
self._invalidate_ssl_cache()
|
|
133
186
|
|
|
134
187
|
except MqttError as e:
|
|
135
188
|
_LOGGER.warning("Harbor: MQTT error for %s: %s (reconnecting)", self.config.serial, e)
|
|
136
189
|
_LOGGER.info("Harbor: MQTT disconnected from camera %s", self.config.serial)
|
|
137
190
|
# Clear SSL context on MQTT error
|
|
138
|
-
|
|
139
|
-
del self.ssl_context_cache[self.config.serial]
|
|
191
|
+
self._invalidate_ssl_cache()
|
|
140
192
|
|
|
141
193
|
except OSError as e:
|
|
142
194
|
_LOGGER.warning("Harbor: MQTT OS error for %s: %s (reconnecting)", self.config.serial, e)
|
|
143
195
|
# Critical to clear context here for WinError 10065 cleanup
|
|
144
|
-
|
|
145
|
-
del self.ssl_context_cache[self.config.serial]
|
|
196
|
+
self._invalidate_ssl_cache()
|
|
146
197
|
except asyncio.CancelledError:
|
|
147
198
|
raise
|
|
148
199
|
except Exception as e:
|
|
149
200
|
_LOGGER.error("Harbor: MQTT unexpected error for %s: %s (reconnecting)", self.config.serial, e)
|
|
150
201
|
# Clear context on unexpected errors too
|
|
151
|
-
|
|
152
|
-
del self.ssl_context_cache[self.config.serial]
|
|
202
|
+
self._invalidate_ssl_cache()
|
|
153
203
|
import traceback
|
|
154
204
|
|
|
155
205
|
_LOGGER.error(traceback.format_exc())
|
|
@@ -195,6 +245,10 @@ class HarborMQTTClient:
|
|
|
195
245
|
except asyncio.CancelledError:
|
|
196
246
|
pass
|
|
197
247
|
_LOGGER.info("Harbor: MQTT client stopped for camera %s", self.config.serial)
|
|
248
|
+
# An intentional stop is a stable disconnect: skip the grace period.
|
|
249
|
+
self._cancel_disconnect_grace()
|
|
250
|
+
if self._reported_connected:
|
|
251
|
+
await self._notify_connection_change(False)
|
|
198
252
|
|
|
199
253
|
def __del__(self) -> None:
|
|
200
254
|
if self._stop_event and not self._stop_event.is_set():
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import ssl
|
|
5
|
+
import tempfile
|
|
6
|
+
|
|
7
|
+
from .config import HarborCameraConfig
|
|
8
|
+
|
|
9
|
+
_LOGGER = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_camera_host(camera_config: HarborCameraConfig) -> str:
|
|
13
|
+
if camera_config.ip_address:
|
|
14
|
+
return camera_config.ip_address
|
|
15
|
+
return f"harborc-{camera_config.serial}.local"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_ssl_cache_key(camera_config: HarborCameraConfig) -> str:
|
|
19
|
+
"""Return the cache key for the SSL context built from this config.
|
|
20
|
+
|
|
21
|
+
Keyed off the certificate material itself so a config carrying new
|
|
22
|
+
credentials never reuses a stale context.
|
|
23
|
+
"""
|
|
24
|
+
if camera_config.cert_pem is not None and camera_config.key_pem is not None:
|
|
25
|
+
digest = hashlib.sha256()
|
|
26
|
+
digest.update(camera_config.cert_pem.encode())
|
|
27
|
+
digest.update(b"\x00")
|
|
28
|
+
digest.update(camera_config.key_pem.encode())
|
|
29
|
+
return f"pem:{digest.hexdigest()}"
|
|
30
|
+
return f"path:{camera_config.cert_path}:{camera_config.key_path}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _write_private_file(path: str, data: str) -> None:
|
|
34
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
35
|
+
# PEM must stay byte-exact: no platform newline translation or locale encoding.
|
|
36
|
+
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as handle:
|
|
37
|
+
handle.write(data)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_ssl_context(camera_config: HarborCameraConfig) -> ssl.SSLContext:
|
|
41
|
+
ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
|
42
|
+
|
|
43
|
+
if camera_config.cert_pem is not None and camera_config.key_pem is not None:
|
|
44
|
+
_LOGGER.info("Harbor: Building SSL context from in-memory PEM data for camera %s", camera_config.serial)
|
|
45
|
+
# ssl.SSLContext.load_cert_chain only accepts file paths, so stage the
|
|
46
|
+
# PEM data in a short-lived private temp dir that is removed before
|
|
47
|
+
# this function returns.
|
|
48
|
+
with tempfile.TemporaryDirectory(prefix="harbor-tls-") as tmp_dir:
|
|
49
|
+
cert_file = os.path.join(tmp_dir, "cert.pem")
|
|
50
|
+
key_file = os.path.join(tmp_dir, "key.pem")
|
|
51
|
+
_write_private_file(cert_file, camera_config.cert_pem)
|
|
52
|
+
_write_private_file(key_file, camera_config.key_pem)
|
|
53
|
+
ctx.load_cert_chain(certfile=cert_file, keyfile=key_file)
|
|
54
|
+
else:
|
|
55
|
+
_LOGGER.info(
|
|
56
|
+
"Harbor: Building SSL context with cert_path=%s, key_path=%s",
|
|
57
|
+
camera_config.cert_path,
|
|
58
|
+
camera_config.key_path,
|
|
59
|
+
)
|
|
60
|
+
if camera_config.cert_path is None or camera_config.key_path is None:
|
|
61
|
+
raise ValueError("HarborCameraConfig has no certificate material")
|
|
62
|
+
ctx.load_cert_chain(certfile=camera_config.cert_path, keyfile=camera_config.key_path)
|
|
63
|
+
|
|
64
|
+
ctx.check_hostname = False
|
|
65
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
66
|
+
return ctx
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_ssl_context(camera_config: HarborCameraConfig, cache: dict | None = None) -> ssl.SSLContext:
|
|
70
|
+
if cache is None:
|
|
71
|
+
_LOGGER.info("Harbor: Creating new SSL context (no cache)")
|
|
72
|
+
return build_ssl_context(camera_config)
|
|
73
|
+
|
|
74
|
+
key = get_ssl_cache_key(camera_config)
|
|
75
|
+
if key in cache:
|
|
76
|
+
_LOGGER.debug("Harbor: Returning cached SSL context for camera %s", camera_config.serial)
|
|
77
|
+
return cache[key]
|
|
78
|
+
|
|
79
|
+
_LOGGER.info("Harbor: Creating new SSL context for camera %s", camera_config.serial)
|
|
80
|
+
ctx = build_ssl_context(camera_config)
|
|
81
|
+
cache[key] = ctx
|
|
82
|
+
return ctx
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import ssl
|
|
3
|
-
|
|
4
|
-
from .config import HarborCameraConfig
|
|
5
|
-
|
|
6
|
-
_LOGGER = logging.getLogger(__name__)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def get_camera_host(camera_config: HarborCameraConfig) -> str:
|
|
10
|
-
if camera_config.ip_address:
|
|
11
|
-
return camera_config.ip_address
|
|
12
|
-
return f"harborc-{camera_config.serial}.local"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def build_ssl_context(camera_config: HarborCameraConfig) -> ssl.SSLContext:
|
|
16
|
-
_LOGGER.info(
|
|
17
|
-
"Harbor: Building SSL context with cert_path=%s, key_path=%s", camera_config.cert_path, camera_config.key_path
|
|
18
|
-
)
|
|
19
|
-
ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
|
20
|
-
ctx.load_cert_chain(certfile=camera_config.cert_path, keyfile=camera_config.key_path)
|
|
21
|
-
ctx.check_hostname = False
|
|
22
|
-
ctx.verify_mode = ssl.CERT_NONE
|
|
23
|
-
return ctx
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def get_ssl_context(camera_config: HarborCameraConfig, cache: dict | None = None) -> ssl.SSLContext:
|
|
27
|
-
if cache is None:
|
|
28
|
-
_LOGGER.info("Harbor: Creating new SSL context (no cache)")
|
|
29
|
-
return build_ssl_context(camera_config)
|
|
30
|
-
|
|
31
|
-
key = camera_config.serial
|
|
32
|
-
if key in cache:
|
|
33
|
-
_LOGGER.debug("Harbor: Returning cached SSL context for camera %s", camera_config.serial)
|
|
34
|
-
return cache[key]
|
|
35
|
-
|
|
36
|
-
_LOGGER.info("Harbor: Creating new SSL context for camera %s", camera_config.serial)
|
|
37
|
-
ctx = build_ssl_context(camera_config)
|
|
38
|
-
cache[key] = ctx
|
|
39
|
-
return ctx
|
|
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
|