span-panel-api 2.3.0__tar.gz → 2.3.2__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.
Files changed (22) hide show
  1. {span_panel_api-2.3.0 → span_panel_api-2.3.2}/PKG-INFO +1 -1
  2. {span_panel_api-2.3.0 → span_panel_api-2.3.2}/pyproject.toml +1 -1
  3. {span_panel_api-2.3.0 → span_panel_api-2.3.2}/src/span_panel_api/__init__.py +13 -2
  4. {span_panel_api-2.3.0 → span_panel_api-2.3.2}/src/span_panel_api/auth.py +117 -0
  5. {span_panel_api-2.3.0 → span_panel_api-2.3.2}/src/span_panel_api/mqtt/connection.py +6 -1
  6. {span_panel_api-2.3.0 → span_panel_api-2.3.2}/LICENSE +0 -0
  7. {span_panel_api-2.3.0 → span_panel_api-2.3.2}/README.md +0 -0
  8. {span_panel_api-2.3.0 → span_panel_api-2.3.2}/src/span_panel_api/const.py +0 -0
  9. {span_panel_api-2.3.0 → span_panel_api-2.3.2}/src/span_panel_api/detection.py +0 -0
  10. {span_panel_api-2.3.0 → span_panel_api-2.3.2}/src/span_panel_api/exceptions.py +0 -0
  11. {span_panel_api-2.3.0 → span_panel_api-2.3.2}/src/span_panel_api/factory.py +0 -0
  12. {span_panel_api-2.3.0 → span_panel_api-2.3.2}/src/span_panel_api/models.py +0 -0
  13. {span_panel_api-2.3.0 → span_panel_api-2.3.2}/src/span_panel_api/mqtt/__init__.py +0 -0
  14. {span_panel_api-2.3.0 → span_panel_api-2.3.2}/src/span_panel_api/mqtt/async_client.py +0 -0
  15. {span_panel_api-2.3.0 → span_panel_api-2.3.2}/src/span_panel_api/mqtt/client.py +0 -0
  16. {span_panel_api-2.3.0 → span_panel_api-2.3.2}/src/span_panel_api/mqtt/const.py +0 -0
  17. {span_panel_api-2.3.0 → span_panel_api-2.3.2}/src/span_panel_api/mqtt/field_metadata.py +0 -0
  18. {span_panel_api-2.3.0 → span_panel_api-2.3.2}/src/span_panel_api/mqtt/homie.py +0 -0
  19. {span_panel_api-2.3.0 → span_panel_api-2.3.2}/src/span_panel_api/mqtt/models.py +0 -0
  20. {span_panel_api-2.3.0 → span_panel_api-2.3.2}/src/span_panel_api/phase_validation.py +0 -0
  21. {span_panel_api-2.3.0 → span_panel_api-2.3.2}/src/span_panel_api/protocol.py +0 -0
  22. {span_panel_api-2.3.0 → span_panel_api-2.3.2}/src/span_panel_api/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: span-panel-api
3
- Version: 2.3.0
3
+ Version: 2.3.2
4
4
  Summary: A client library for SPAN Panel API
5
5
  License-File: LICENSE
6
6
  Author: SpanPanel
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "span-panel-api"
3
- version = "2.3.0"
3
+ version = "2.3.2"
4
4
  description = "A client library for SPAN Panel API"
5
5
  authors = [
6
6
  {name = "SpanPanel"}
@@ -4,7 +4,15 @@ A modern, type-safe Python client library for the SPAN Panel API,
4
4
  supporting MQTT/Homie (v2) transport.
5
5
  """
6
6
 
7
- from .auth import download_ca_cert, get_homie_schema, regenerate_passphrase, register_v2
7
+ from .auth import (
8
+ delete_fqdn,
9
+ download_ca_cert,
10
+ get_fqdn,
11
+ get_homie_schema,
12
+ regenerate_passphrase,
13
+ register_fqdn,
14
+ register_v2,
15
+ )
8
16
  from .detection import DetectionResult, detect_api_version
9
17
  from .exceptions import (
10
18
  SpanPanelAPIError,
@@ -44,7 +52,7 @@ from .protocol import (
44
52
  StreamingCapableProtocol,
45
53
  )
46
54
 
47
- __version__ = "2.3.0"
55
+ __version__ = "2.3.2"
48
56
  # fmt: off
49
57
  __all__ = [ # noqa: RUF022
50
58
  # Protocols
@@ -70,8 +78,11 @@ __all__ = [ # noqa: RUF022
70
78
  "V2AuthResponse",
71
79
  "V2HomieSchema",
72
80
  "V2StatusInfo",
81
+ "delete_fqdn",
73
82
  "download_ca_cert",
83
+ "get_fqdn",
74
84
  "get_homie_schema",
85
+ "register_fqdn",
75
86
  "regenerate_passphrase",
76
87
  "register_v2",
77
88
  # Transport
@@ -241,6 +241,123 @@ async def regenerate_passphrase(host: str, token: str, timeout: float = 10.0, po
241
241
  return _str(data["ebusBrokerPassword"])
242
242
 
243
243
 
244
+ async def register_fqdn(host: str, token: str, fqdn: str, timeout: float = 10.0, port: int = 80) -> None:
245
+ """Register an FQDN with the SPAN Panel for TLS certificate SAN inclusion.
246
+
247
+ The panel regenerates its TLS server certificate to include the
248
+ provided FQDN in the Subject Alternative Names, allowing MQTTS
249
+ clients connecting via the FQDN to pass hostname verification.
250
+
251
+ Args:
252
+ host: IP address or hostname of the SPAN Panel
253
+ token: Valid JWT access token from register_v2
254
+ fqdn: Fully qualified domain name to register
255
+ timeout: Request timeout in seconds
256
+ port: HTTP port of the panel bootstrap API
257
+
258
+ Raises:
259
+ SpanPanelAuthError: Token invalid or expired
260
+ SpanPanelConnectionError: Cannot reach panel
261
+ SpanPanelTimeoutError: Request timed out
262
+ SpanPanelAPIError: Unexpected response (including 404 if unsupported)
263
+ """
264
+ url = _build_url(host, port, "/api/v2/dns/fqdn")
265
+ headers = {"Authorization": f"Bearer {token}"}
266
+ payload = {"ebusTlsFqdn": fqdn}
267
+
268
+ try:
269
+ async with httpx.AsyncClient(timeout=timeout, verify=False) as client: # nosec B501
270
+ response = await client.post(url, json=payload, headers=headers)
271
+ except httpx.ConnectError as exc:
272
+ raise SpanPanelConnectionError(f"Cannot reach panel at {host}") from exc
273
+ except httpx.TimeoutException as exc:
274
+ raise SpanPanelTimeoutError(f"Timed out connecting to {host}") from exc
275
+
276
+ if response.status_code in (401, 403):
277
+ raise SpanPanelAuthError(f"Authentication failed (HTTP {response.status_code})")
278
+
279
+ if response.status_code not in (200, 201, 204):
280
+ raise SpanPanelAPIError(f"Failed to register FQDN: HTTP {response.status_code}")
281
+
282
+
283
+ async def get_fqdn(host: str, token: str, timeout: float = 10.0, port: int = 80) -> str:
284
+ """Retrieve the currently registered FQDN from the SPAN Panel.
285
+
286
+ Args:
287
+ host: IP address or hostname of the SPAN Panel
288
+ token: Valid JWT access token from register_v2
289
+ timeout: Request timeout in seconds
290
+ port: HTTP port of the panel bootstrap API
291
+
292
+ Returns:
293
+ The registered FQDN, or empty string if none is configured
294
+
295
+ Raises:
296
+ SpanPanelAuthError: Token invalid or expired
297
+ SpanPanelConnectionError: Cannot reach panel
298
+ SpanPanelTimeoutError: Request timed out
299
+ SpanPanelAPIError: Unexpected response
300
+ """
301
+ url = _build_url(host, port, "/api/v2/dns/fqdn")
302
+ headers = {"Authorization": f"Bearer {token}"}
303
+
304
+ try:
305
+ async with httpx.AsyncClient(timeout=timeout, verify=False) as client: # nosec B501
306
+ response = await client.get(url, headers=headers)
307
+ except httpx.ConnectError as exc:
308
+ raise SpanPanelConnectionError(f"Cannot reach panel at {host}") from exc
309
+ except httpx.TimeoutException as exc:
310
+ raise SpanPanelTimeoutError(f"Timed out connecting to {host}") from exc
311
+
312
+ if response.status_code in (401, 403):
313
+ raise SpanPanelAuthError(f"Authentication failed (HTTP {response.status_code})")
314
+
315
+ if response.status_code == 404:
316
+ return ""
317
+
318
+ if response.status_code != 200:
319
+ raise SpanPanelAPIError(f"Failed to get FQDN: HTTP {response.status_code}")
320
+
321
+ data: dict[str, object] = response.json()
322
+ return _str(data.get("ebusTlsFqdn"))
323
+
324
+
325
+ async def delete_fqdn(host: str, token: str, timeout: float = 10.0, port: int = 80) -> None:
326
+ """Remove the registered FQDN from the SPAN Panel.
327
+
328
+ The panel regenerates its TLS certificate without the FQDN in
329
+ the SAN list.
330
+
331
+ Args:
332
+ host: IP address or hostname of the SPAN Panel
333
+ token: Valid JWT access token from register_v2
334
+ timeout: Request timeout in seconds
335
+ port: HTTP port of the panel bootstrap API
336
+
337
+ Raises:
338
+ SpanPanelAuthError: Token invalid or expired
339
+ SpanPanelConnectionError: Cannot reach panel
340
+ SpanPanelTimeoutError: Request timed out
341
+ SpanPanelAPIError: Unexpected response
342
+ """
343
+ url = _build_url(host, port, "/api/v2/dns/fqdn")
344
+ headers = {"Authorization": f"Bearer {token}"}
345
+
346
+ try:
347
+ async with httpx.AsyncClient(timeout=timeout, verify=False) as client: # nosec B501
348
+ response = await client.delete(url, headers=headers)
349
+ except httpx.ConnectError as exc:
350
+ raise SpanPanelConnectionError(f"Cannot reach panel at {host}") from exc
351
+ except httpx.TimeoutException as exc:
352
+ raise SpanPanelTimeoutError(f"Timed out connecting to {host}") from exc
353
+
354
+ if response.status_code in (401, 403):
355
+ raise SpanPanelAuthError(f"Authentication failed (HTTP {response.status_code})")
356
+
357
+ if response.status_code not in (200, 204):
358
+ raise SpanPanelAPIError(f"Failed to delete FQDN: HTTP {response.status_code}")
359
+
360
+
244
361
  async def get_v2_status(host: str, timeout: float = 5.0, port: int = 80) -> V2StatusInfo:
245
362
  """Lightweight v2 status probe (unauthenticated).
246
363
 
@@ -175,7 +175,12 @@ class AsyncMqttBridge:
175
175
  self._client.on_socket_open = self._on_socket_open_sync
176
176
  self._client.on_socket_register_write = self._on_socket_register_write_sync
177
177
  _LOGGER.debug("BRIDGE: Running TLS+connect in executor to %s:%s", self._host, self._port)
178
- await self._loop.run_in_executor(None, _blocking_tls_and_connect)
178
+ try:
179
+ await self._loop.run_in_executor(None, _blocking_tls_and_connect)
180
+ except OSError as exc:
181
+ raise SpanPanelConnectionError(
182
+ f"Cannot connect to MQTT broker at {self._host}:{self._port}: {exc}"
183
+ ) from exc
179
184
  _LOGGER.debug("BRIDGE: Executor connect returned, waiting for CONNACK...")
180
185
  finally:
181
186
  # Switch to async-only socket callbacks now that we are
File without changes
File without changes