span-panel-api 3.4.0__tar.gz → 3.4.1__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 (103) hide show
  1. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/CHANGELOG.md +10 -0
  2. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/PKG-INFO +1 -1
  3. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/pyproject.toml +1 -1
  4. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/src/span_panel_api/_http.py +33 -16
  5. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_plaintext_warning.py +50 -1
  6. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/.gitignore +0 -0
  7. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/LICENSE +0 -0
  8. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/README.md +0 -0
  9. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/src/span_panel_api/__init__.py +0 -0
  10. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/src/span_panel_api/_ssl.py +0 -0
  11. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/src/span_panel_api/adapters.py +0 -0
  12. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/src/span_panel_api/auth.py +0 -0
  13. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/src/span_panel_api/const.py +0 -0
  14. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/src/span_panel_api/detection.py +0 -0
  15. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/src/span_panel_api/dispatch.py +0 -0
  16. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/src/span_panel_api/exceptions.py +0 -0
  17. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/src/span_panel_api/factory.py +0 -0
  18. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/src/span_panel_api/models.py +0 -0
  19. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/src/span_panel_api/mqtt/__init__.py +0 -0
  20. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/src/span_panel_api/mqtt/async_client.py +0 -0
  21. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/src/span_panel_api/mqtt/client.py +0 -0
  22. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/src/span_panel_api/mqtt/connection.py +0 -0
  23. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/src/span_panel_api/mqtt/const.py +0 -0
  24. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/src/span_panel_api/mqtt/control.py +0 -0
  25. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/src/span_panel_api/mqtt/models.py +0 -0
  26. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/src/span_panel_api/phase_validation.py +0 -0
  27. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/src/span_panel_api/protocol.py +0 -0
  28. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/src/span_panel_api/py.typed +0 -0
  29. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/src/span_panel_api/schema_drift.py +0 -0
  30. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/conftest.py +0 -0
  31. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/fixtures/configs/simulation_config_32_circuit.yaml +0 -0
  32. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/fixtures/configs/simulation_config_40_circuit_with_battery.yaml +0 -0
  33. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/fixtures/configs/simulation_config_8_tab_workshop.yaml +0 -0
  34. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/fixtures/flat_wire.json +0 -0
  35. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/fixtures/panelbench_unvalued_by_both.json +0 -0
  36. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/fixtures/panelbench_wire.json +0 -0
  37. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/fixtures/v2/README.md +0 -0
  38. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/fixtures/v2/status.json +0 -0
  39. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/reference_payloads/README.md +0 -0
  40. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/reference_payloads/__init__.py +0 -0
  41. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/reference_payloads/bootstrap.py +0 -0
  42. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/reference_payloads/schema_one.py +0 -0
  43. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/simulation_fixtures/circuits.response.txt +0 -0
  44. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/simulation_fixtures/panel.response.txt +0 -0
  45. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/simulation_fixtures/soe.response.txt +0 -0
  46. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/simulation_fixtures/status.response.txt +0 -0
  47. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_absent_readings_are_not_zero.py +0 -0
  48. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_accumulator.py +0 -0
  49. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_adapters_discovery.py +0 -0
  50. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_adopted_control.py +0 -0
  51. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_adoption.py +0 -0
  52. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_async_mqtt_client.py +0 -0
  53. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_auth_and_homie_helpers.py +0 -0
  54. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_auth_redaction.py +0 -0
  55. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_ca_pinning.py +0 -0
  56. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_catalog_divergence.py +0 -0
  57. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_control_interceptor.py +0 -0
  58. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_detection_auth.py +0 -0
  59. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_exceptions.py +0 -0
  60. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_factory_dispatch.py +0 -0
  61. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_field_metadata.py +0 -0
  62. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_https_transport.py +0 -0
  63. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_leaf_name_mismatch.py +0 -0
  64. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_live_flat_differential.py +0 -0
  65. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_mqtt_bridge.py +0 -0
  66. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_mqtt_client_connection.py +0 -0
  67. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_mqtt_connect_flow.py +0 -0
  68. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_mqtt_debounce.py +0 -0
  69. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_mqtt_homie.py +0 -0
  70. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_packaging.py +0 -0
  71. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_phase_validation_configs.py +0 -0
  72. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_phase_validation_errors.py +0 -0
  73. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_protocol_conformance.py +0 -0
  74. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_protocol_models.py +0 -0
  75. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_public_api_unchanged.py +0 -0
  76. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_publish_outcome.py +0 -0
  77. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_redispatch_on_reconnect.py +0 -0
  78. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_reference_tree_values.py +0 -0
  79. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_rest_transport_contract.py +0 -0
  80. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_schema_fetch_transport_split.py +0 -0
  81. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_schema_generation_cross_check.py +0 -0
  82. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_schema_migration_delta.py +0 -0
  83. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_schema_one_adapter.py +0 -0
  84. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_schema_one_charge_limit.py +0 -0
  85. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_schema_one_circuits.py +0 -0
  86. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_schema_one_conformance.py +0 -0
  87. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_schema_one_connection_health.py +0 -0
  88. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_schema_one_control_refusal.py +0 -0
  89. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_schema_one_devices.py +0 -0
  90. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_schema_one_discovery.py +0 -0
  91. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_schema_one_extension.py +0 -0
  92. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_schema_one_panel.py +0 -0
  93. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_schema_one_pcs.py +0 -0
  94. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_schema_one_service_entrance.py +0 -0
  95. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_schema_one_shed_forecast.py +0 -0
  96. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_schema_one_snapshot.py +0 -0
  97. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_schema_one_transport.py +0 -0
  98. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_schema_provenance.py +0 -0
  99. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_schema_zero_adapter.py +0 -0
  100. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_shared_http_client.py +0 -0
  101. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_ssl_context.py +0 -0
  102. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/test_v2_status_parser.py +0 -0
  103. {span_panel_api-3.4.0 → span_panel_api-3.4.1}/tests/tls_fixtures.py +0 -0
@@ -7,6 +7,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
7
7
  Pre-releases are not listed separately. A beta is a step towards the next public version, so its changes are folded into that version's entry as they land and are described against the **last public release**, never against the beta before it. What one
8
8
  beta corrected in an earlier beta does not appear at all: from the point of view of somebody upgrading between released versions, it never happened.
9
9
 
10
+ ## [3.4.1]
11
+
12
+ A panel that merely advertises itself on the network no longer produces the plaintext-transport warning when discovery probes it, closing the remaining way issue span#264's log line reached an operator who could do nothing about it.
13
+
14
+ ### Changed
15
+
16
+ - **The status endpoint no longer emits the plaintext-transport warning**, for the CA download's reason from the other side: it is the detection probe made against devices nobody has configured, where no pin can exist and the only action is configuring the
17
+ panel — whose flow pins before any credential moves. It carries no credential in either direction, and it no longer spends the once-per-host warning slot, which it previously claimed first in every flow so that a genuinely credential-bearing call behind
18
+ it said nothing. Registration, passphrase rotation, and the schema fetch warn exactly as before.
19
+
10
20
  ## [3.4.0]
11
21
 
12
22
  A consumer that pinned the panel's CA could not put its schema fetches behind that pin, because the one port `SpanMqttClient` took served two transports with opposite security properties — the schema fetch, which should ride the pinned HTTPS transport, and
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: span-panel-api
3
- Version: 3.4.0
3
+ Version: 3.4.1
4
4
  Summary: A client library for SPAN Panel API
5
5
  Project-URL: Homepage, https://github.com/SpanPanel/span-panel-api
6
6
  Project-URL: Issues, https://github.com/SpanPanel/span-panel-api/issues
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "span-panel-api"
3
- version = "3.4.0"
3
+ version = "3.4.1"
4
4
  description = "A client library for SPAN Panel API"
5
5
  authors = [
6
6
  {name = "SpanPanel"}
@@ -31,11 +31,14 @@ DEFAULT_HTTPS_PORT = 443
31
31
  #: The one bootstrap path two modules request: the detector probes it to decide
32
32
  #: whether the panel speaks v2 at all, and `get_v2_status` reads the same answer
33
33
  #: for a caller that already knows it does. Named here rather than spelled out in
34
- #: each, so the two cannot drift apart the way their parsers had.
34
+ #: each, so the two cannot drift apart the way their parsers had. Load-bearing
35
+ #: for `_warn_plaintext_transport`'s exemption: a request to this path is the
36
+ #: one the warning stays silent for, so a change here changes what warns.
35
37
  V2_STATUS_PATH = "/api/v2/status"
36
38
 
37
- #: The one bootstrap path exempt from the plaintext warning, named here because
38
- #: the transport is what grants the exemption. See `_warn_plaintext_transport`.
39
+ #: Exempt from the plaintext warning alongside `V2_STATUS_PATH`, named here
40
+ #: because the transport is what grants the exemption. See
41
+ #: `_warn_plaintext_transport`.
39
42
  CA_CERT_PATH = "/api/v2/certificate/ca"
40
43
 
41
44
  #: The verbs the bootstrap API uses. Spelled as a `Literal` rather than passed
@@ -174,19 +177,33 @@ def _reset_plaintext_warnings() -> None:
174
177
  def _warn_plaintext_transport(host: str, path: str, ssl_context: ssl.SSLContext | None) -> None:
175
178
  """Say out loud, once per panel, that its bootstrap traffic is not encrypted.
176
179
 
177
- **The CA download is exempt, and does not claim the once-per-host slot.**
180
+ **Two endpoints are exempt, and neither claims the once-per-host slot.**
178
181
  The warning exists so an operator can tell a security property is off when
179
- it could be on, and for that endpoint there is no "on": verifying the fetch
180
- of the anchor would require the anchor being fetched, an unverified-TLS
181
- wrapping is readable and forgeable by the same active on-path attacker, and
182
- the payload is a public certificate carrying no credential in either
183
- direction — its authenticity control is the leaf check callers run *after*
184
- the fetch. Each caller also states its own trust posture in its own voice:
185
- the bridge's unpinned warning, a config flow's fingerprint confirmation, a
186
- consumer's trust-on-first-use log. Warning here anyway named credentials the
187
- call never carries, which is the line issue span#264 reported. Not marking
188
- the host matters as much as not warning: a pinned consumer's diagnostic
189
- re-read must not spend the slot a genuinely plaintext call needs later.
182
+ it could be on, and for both there is no "on". The CA download fetches the
183
+ very anchor verification would need — an unverified-TLS wrapping is
184
+ readable and forgeable by the same active on-path attacker, the payload is
185
+ a public certificate, and its authenticity control is the leaf check
186
+ callers run *after* the fetch. The status endpoint is the detection probe:
187
+ most prominently the request discovery makes against a device nobody has
188
+ configured, where no pin can exist because trust-on-first-use has not
189
+ happened and the only action available is configuring the panel — whose
190
+ flow pins before any credential moves, with a person confirming the
191
+ fingerprint. Neither call carries a credential in either direction, and
192
+ warning on them named credentials they never carry: the CA download's line
193
+ is what issue span#264 reported, and the status probe's is what a merely
194
+ *advertising* unconfigured panel produced at every boot.
195
+
196
+ Two limits of the status exemption, stated rather than implied. The body
197
+ informs decisions — `proximityProven`, the serial an identity check reads —
198
+ and a plaintext answer is one anything on the path can write; the controls
199
+ for that are the panel's own registration gate and the pin the consumer's
200
+ flow acquires before a credential moves, not a log line. And a consumer
201
+ *can* probe a configured, pinned panel's status without its context; the
202
+ exemption means no warning will point that out, so a consumer owes every
203
+ probe of a configured host the entry's own transport. Not marking the host matters as
204
+ much as not warning — the probe runs first in every flow and a diagnostic
205
+ re-read runs on pinned entries, and neither may spend the slot a
206
+ credential-bearing call needs later.
190
207
 
191
208
  In the same voice as the MQTT bridge's unpinned-CA warning, and for the same
192
209
  reason: a security property that is off by default is only a decision if the
@@ -214,7 +231,7 @@ def _warn_plaintext_transport(host: str, path: str, ssl_context: ssl.SSLContext
214
231
  """
215
232
  if ssl_context is not None:
216
233
  return
217
- if path == CA_CERT_PATH:
234
+ if path in (CA_CERT_PATH, V2_STATUS_PATH):
218
235
  return
219
236
  if host in _warned_plaintext_hosts:
220
237
  return
@@ -26,7 +26,8 @@ from unittest.mock import AsyncMock, patch
26
26
  import httpx
27
27
  import pytest
28
28
 
29
- from span_panel_api.auth import download_ca_cert, regenerate_passphrase, register_v2
29
+ from span_panel_api.auth import download_ca_cert, get_v2_status, regenerate_passphrase, register_v2
30
+ from span_panel_api.detection import detect_api_version
30
31
  from span_panel_api.mqtt.models import MqttClientConfig
31
32
 
32
33
  HOST = "panel.invalid"
@@ -166,6 +167,54 @@ class TestItIsSaidOnce:
166
167
 
167
168
  assert len(_warnings(caplog)) == 0
168
169
 
170
+ @pytest.mark.asyncio
171
+ async def test_the_status_probe_never_warns(self, caplog: pytest.LogCaptureFixture) -> None:
172
+ """Exempted in 3.4.1, for the CA download's reason from the other side.
173
+
174
+ The status endpoint is the detection probe — the request zeroconf
175
+ discovery makes against a device nobody has configured, once per boot,
176
+ where no pin can exist because trust-on-first-use has not happened and
177
+ there is nothing the operator can do but configure the panel. It
178
+ carries no credential in either direction, and the flows that probe it
179
+ on a configured-but-unpinned entry acquire the pin before any
180
+ credential moves, with a human confirming the fingerprint. The warning
181
+ here named credentials the call never carries and pointed at an action
182
+ nobody could take — the line that made an operator remove a healthy
183
+ emulator. The exemption's limits live in `_warn_plaintext_transport`'s
184
+ docstring: a consumer probing a *configured* host owes that probe the
185
+ entry's own transport, because no warning will say so anymore.
186
+ """
187
+ answer = _json_response({"serialNumber": "SYN-0000-0001", "firmwareVersion": "f"}, method="GET")
188
+ with caplog.at_level(logging.WARNING):
189
+ await get_v2_status(HOST, httpx_client=_client("get", answer))
190
+ assert len(_warnings(caplog)) == 0
191
+
192
+ @pytest.mark.asyncio
193
+ async def test_the_detection_probe_never_warns(self, caplog: pytest.LogCaptureFixture) -> None:
194
+ """Detection reads the same endpoint, and must be equally silent."""
195
+ answer = _json_response({"serialNumber": "SYN-0000-0001", "firmwareVersion": "f"}, method="GET")
196
+ with caplog.at_level(logging.WARNING):
197
+ await detect_api_version(HOST, httpx_client=_client("get", answer))
198
+ assert len(_warnings(caplog)) == 0
199
+
200
+ @pytest.mark.asyncio
201
+ async def test_the_status_probe_does_not_swallow_a_later_warning(self, caplog: pytest.LogCaptureFixture) -> None:
202
+ """The probe fires first in every flow, so it must not spend the slot.
203
+
204
+ Today it does exactly that: a reauth's status probe claims the
205
+ once-per-host warning, and the credential-bearing call behind it says
206
+ nothing. The warning belongs to whichever call actually deserves it.
207
+ """
208
+ status = _json_response({"serialNumber": "SYN-0000-0001", "firmwareVersion": "f"}, method="GET")
209
+ rotate = _json_response({"ebusBrokerPassword": "new-pass"}, method="PUT")
210
+ with caplog.at_level(logging.WARNING):
211
+ await get_v2_status(HOST, httpx_client=_client("get", status))
212
+ # The midpoint is the assertion: the slot must still be unspent
213
+ # here, so the warning below demonstrably belongs to the rotation.
214
+ assert len(_warnings(caplog)) == 0
215
+ await regenerate_passphrase(HOST, "jwt", httpx_client=_client("put", rotate))
216
+ assert len(_warnings(caplog)) == 1
217
+
169
218
  @pytest.mark.asyncio
170
219
  async def test_the_ca_download_does_not_swallow_a_later_warning(self, caplog: pytest.LogCaptureFixture) -> None:
171
220
  """Skipping the warning must not mark the host as already warned.
File without changes
File without changes