Trim verbosity across the codebase #79

Merged
rob merged 70 commits from refactor/trim-verbosity into feat/firmware-updates 2026-09-21 05:48:34 +00:00
Collaborator

Same behaviour, materially less code. Comments and docstrings are cut back to what the code cannot say for itself — spec citations, units, sentinel rules and real traps stay, one line each — and a handful of copied-out patterns fold into one helper apiece, with a few hot-path efficiency fixes that fell out on the way (cached shapes and identifier routing, a keyed alarm store, one manifest pass per request, raw-id pre-filters).

Tests are deliberately untouched: the existing 1123 are the gate for a refactor that must not change behaviour. Across custom_components, updater and tools the three trees go from 18,411 lines to 16,492.

A few places where the result is not a pure deletion:

  • entity_shape.py: the semantic-class guards read isinstance(known, SemanticClass) and known in X rather than a bare membership test, which is what mypy accepts.
  • event.py: the EVENT_TYPES constant stays for RUF012 and mypy; only the property that returned it went.
  • serve_http (updater) calls server_close() after awaiting the serve task, which was the control API's original order. The file server used to close first.
  • session.run (updater) logs a hotspot failure at WARNING without a traceback, where it used to use exception.
  • readings.notify logs a listener that raises under one logger, where the six copies it replaces each used their own module's.
  • The bench's /api/catalogue no longer emits offset, transmitted, messageType or rules. Nothing in static/app.js reads any of them; every other key and value is byte-identical, checked by diffing the rendered JSON against this branch's base.
  • The bench parses a typed IPv4 address with ipaddress.IPv4Address, so 192.168.001.10 is now refused as ambiguous, and a bad address gets one message rather than three specific ones.

pyproject.toml stops requiring a docstring on a function, method or __init__; a module and a class still need one.

Same behaviour, materially less code. Comments and docstrings are cut back to what the code cannot say for itself — spec citations, units, sentinel rules and real traps stay, one line each — and a handful of copied-out patterns fold into one helper apiece, with a few hot-path efficiency fixes that fell out on the way (cached shapes and identifier routing, a keyed alarm store, one manifest pass per request, raw-id pre-filters). Tests are deliberately untouched: the existing 1123 are the gate for a refactor that must not change behaviour. Across `custom_components`, `updater` and `tools` the three trees go from 18,411 lines to 16,492. A few places where the result is not a pure deletion: - `entity_shape.py`: the semantic-class guards read `isinstance(known, SemanticClass) and known in X` rather than a bare membership test, which is what mypy accepts. - `event.py`: the `EVENT_TYPES` constant stays for RUF012 and mypy; only the property that returned it went. - `serve_http` (updater) calls `server_close()` after awaiting the serve task, which was the control API's original order. The file server used to close first. - `session.run` (updater) logs a hotspot failure at WARNING without a traceback, where it used to use `exception`. - `readings.notify` logs a listener that raises under one logger, where the six copies it replaces each used their own module's. - The bench's `/api/catalogue` no longer emits `offset`, `transmitted`, `messageType` or `rules`. Nothing in `static/app.js` reads any of them; every other key and value is byte-identical, checked by diffing the rendered JSON against this branch's base. - The bench parses a typed IPv4 address with `ipaddress.IPv4Address`, so `192.168.001.10` is now refused as ambiguous, and a bad address gets one message rather than three specific ones. `pyproject.toml` stops requiring a docstring on a function, method or `__init__`; a module and a class still need one.
Stop requiring a docstring on every name
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 9s
Lint, type check and test / quality (pull_request) Failing after 19s
Lint, type check and test / release (pull_request) Has been skipped
f265584a0a
Subscribe to a listener list in one place
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 21s
Lint, type check and test / release (pull_request) Has been skipped
83e2b465da
Call a listener list in one place
Some checks failed
Lint, type check and test / quality (pull_request) Failing after 30s
Lint, type check and test / hassfest (pull_request) Successful in 30s
Lint, type check and test / release (pull_request) Has been skipped
f228c69754
Hold the entity availability plumbing in one base
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 12s
Lint, type check and test / quality (pull_request) Failing after 19s
Lint, type check and test / release (pull_request) Has been skipped
4507f18192
Subscribe a redraw directly instead of wrapping it
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 9s
Lint, type check and test / quality (pull_request) Failing after 19s
Lint, type check and test / release (pull_request) Has been skipped
d2049785b0
Test an entity's own key once, then hand off to a hook
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 18s
Lint, type check and test / quality (pull_request) Failing after 30s
Lint, type check and test / release (pull_request) Has been skipped
9ca9ac3f2c
Cache the shape an endpoint's description asks for
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 27s
Lint, type check and test / quality (pull_request) Failing after 30s
Lint, type check and test / release (pull_request) Has been skipped
c8ebea4b86
Read a kind and a semantic class the one way
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 19s
Lint, type check and test / release (pull_request) Has been skipped
e1e0f3e1d1
Keep alarms under the endpoint that raised them
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 9s
Lint, type check and test / quality (pull_request) Failing after 25s
Lint, type check and test / release (pull_request) Has been skipped
a3cc031525
Sort the inventory's nodes when one moves, not on every read
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 10s
Lint, type check and test / quality (pull_request) Failing after 20s
Lint, type check and test / release (pull_request) Has been skipped
46760200e3
Decode a frame's route once for every listener
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 9s
Lint, type check and test / quality (pull_request) Failing after 22s
Lint, type check and test / release (pull_request) Has been skipped
cb35f57299
Ask the store directly where nothing needs the coordinator
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 21s
Lint, type check and test / quality (pull_request) Failing after 40s
Lint, type check and test / release (pull_request) Has been skipped
30b888b0f0
Inline four one-use wrappers and a hand-rolled round-up
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 19s
Lint, type check and test / quality (pull_request) Failing after 26s
Lint, type check and test / release (pull_request) Has been skipped
e45c5abe80
Cut the setup and discovery docstrings to their reasons
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 9s
Lint, type check and test / quality (pull_request) Failing after 20s
Lint, type check and test / release (pull_request) Has been skipped
9036791e51
Cut the entity and reading docstrings to their reasons
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 18s
Lint, type check and test / quality (pull_request) Failing after 29s
Lint, type check and test / release (pull_request) Has been skipped
0f32f9742a
Cut the bus module docstrings to their reasons
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 18s
Lint, type check and test / release (pull_request) Has been skipped
7fccf91540
Cut the platform modules' docstrings to their reasons
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 19s
Lint, type check and test / release (pull_request) Has been skipped
cf11dfe306
Drop a comment that was wrong and one that was long
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 18s
Lint, type check and test / release (pull_request) Has been skipped
c358af8f70
Drop two docstrings the brief marked for the cull
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 20s
Lint, type check and test / release (pull_request) Has been skipped
69d9ad9af9
Drop the protocol package's dead re-export layer
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 16s
Lint, type check and test / quality (pull_request) Failing after 33s
Lint, type check and test / release (pull_request) Has been skipped
6faaf0f231
Import the catalogue's quantities by name
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 9s
Lint, type check and test / quality (pull_request) Failing after 20s
Lint, type check and test / release (pull_request) Has been skipped
7433a03949
Look a message up without an exception per unknown class
Some checks failed
Lint, type check and test / quality (pull_request) Failing after 26s
Lint, type check and test / hassfest (pull_request) Successful in 26s
Lint, type check and test / release (pull_request) Has been cancelled
220c83040c
rob force-pushed refactor/trim-verbosity from 220c83040c
Some checks failed
Lint, type check and test / quality (pull_request) Failing after 26s
Lint, type check and test / hassfest (pull_request) Successful in 26s
Lint, type check and test / release (pull_request) Has been cancelled
to b71645ed69
Some checks failed
Lint, type check and test / quality (pull_request) Failing after 31s
Lint, type check and test / hassfest (pull_request) Successful in 31s
Lint, type check and test / release (pull_request) Has been skipped
2026-09-20 18:14:19 +00:00
Compare
Look a sentinel up rather than scanning for it
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 19s
Lint, type check and test / release (pull_request) Has been skipped
8866bb0281
Let int.from_bytes do the sign extension
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 18s
Lint, type check and test / quality (pull_request) Failing after 30s
Lint, type check and test / release (pull_request) Has been skipped
a1ac34c1f5
Inline two one-use wrappers in the firmware store
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 20s
Lint, type check and test / quality (pull_request) Failing after 27s
Lint, type check and test / release (pull_request) Has been skipped
367b46a279
Take the shared subscribe, notify and route in the OTA store
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 9s
Lint, type check and test / quality (pull_request) Failing after 22s
Lint, type check and test / release (pull_request) Has been skipped
808fe157d7
Cut the firmware and update docstrings to their reasons
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 19s
Lint, type check and test / release (pull_request) Has been skipped
e598e6231f
Cut the catalogue and codec docstrings to their reasons
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 20s
Lint, type check and test / release (pull_request) Has been skipped
14d22d10ec
Cut the remaining protocol docstrings to their reasons
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 10s
Lint, type check and test / quality (pull_request) Failing after 24s
Lint, type check and test / release (pull_request) Has been skipped
dda1705618
Build the node manifest offers once per request
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 9s
Lint, type check and test / quality (pull_request) Failing after 20s
Lint, type check and test / release (pull_request) Has been skipped
58e1ff43d1
Keep the newest image per node in one place
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 9s
Lint, type check and test / quality (pull_request) Failing after 20s
Lint, type check and test / release (pull_request) Has been skipped
f66e31c2bf
Cut the session constants to their spec cites
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 23s
Lint, type check and test / quality (pull_request) Failing after 27s
Lint, type check and test / release (pull_request) Has been skipped
5fb91d914c
Serve both HTTP servers through one coroutine
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 19s
Lint, type check and test / release (pull_request) Has been skipped
37b69c4eb8
Cut the docstrings off the API's link and radio seams
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 9s
Lint, type check and test / quality (pull_request) Failing after 26s
Lint, type check and test / release (pull_request) Has been skipped
03722d5826
Report a hotspot and a bus failure the same way
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 28s
Lint, type check and test / quality (pull_request) Failing after 32s
Lint, type check and test / release (pull_request) Has been skipped
bb47125833
Reach the control API by a cast, as the file server does
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 18s
Lint, type check and test / release (pull_request) Has been skipped
59e8d8cb60
Check the two credential lengths outright
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 20s
Lint, type check and test / release (pull_request) Has been skipped
f872a528e5
Drop the updater's empty requirements file
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 18s
Lint, type check and test / quality (pull_request) Failing after 34s
Lint, type check and test / release (pull_request) Has been skipped
3ce90dea67
Sift frames on the raw identifier before decoding
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 14s
Lint, type check and test / quality (pull_request) Failing after 23s
Lint, type check and test / release (pull_request) Has been skipped
95557fcfc9
Cut the docstrings across the API, bus and config
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 20s
Lint, type check and test / quality (pull_request) Failing after 31s
Lint, type check and test / release (pull_request) Has been skipped
b44db1ed37
Cut the file server and firmware cache docstrings
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 9s
Lint, type check and test / quality (pull_request) Failing after 19s
Lint, type check and test / release (pull_request) Has been skipped
2ecf1c074a
Cut the bus link and radio docstrings
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 25s
Lint, type check and test / quality (pull_request) Failing after 29s
Lint, type check and test / release (pull_request) Has been skipped
565f352ddb
Cut the update session and service docstrings
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 19s
Lint, type check and test / release (pull_request) Has been skipped
bc91222443
Declare the bench's fields positionally
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 22s
Lint, type check and test / release (pull_request) Has been skipped
03c53fa5e1
Render a bench field from its own dataclass fields
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 21s
Lint, type check and test / release (pull_request) Has been skipped
23360673fe
Stop rendering rules the browser never reads
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 24s
Lint, type check and test / quality (pull_request) Failing after 27s
Lint, type check and test / release (pull_request) Has been skipped
b043e2c4fc
Take a bench field's scale from the quantity table
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 19s
Lint, type check and test / release (pull_request) Has been skipped
8081d0ba8a
Default a message's priority to its class
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 19s
Lint, type check and test / release (pull_request) Has been skipped
c6f8ce87b0
Fall back to a field's own default when none is typed
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 19s
Lint, type check and test / release (pull_request) Has been skipped
8eb067d8f3
Parse an address with the standard library
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 19s
Lint, type check and test / release (pull_request) Has been skipped
fed5875d85
Work out the carried fields once per build
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 30s
Lint, type check and test / quality (pull_request) Failing after 32s
Lint, type check and test / release (pull_request) Has been skipped
857d541331
Wait on the stop event itself between steps
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 9s
Lint, type check and test / quality (pull_request) Failing after 20s
Lint, type check and test / release (pull_request) Has been skipped
236f60b7b7
Guess a static file's type from its name
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 9s
Lint, type check and test / quality (pull_request) Failing after 22s
Lint, type check and test / release (pull_request) Has been skipped
99e7e4c557
Build the catalogue once
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 18s
Lint, type check and test / quality (pull_request) Failing after 34s
Lint, type check and test / release (pull_request) Has been skipped
6aa95bb395
Cut the bench catalogue's docstrings
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 9s
Lint, type check and test / quality (pull_request) Failing after 28s
Lint, type check and test / release (pull_request) Has been skipped
6d9fc2ca10
Cut the bench encoder's docstrings
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 25s
Lint, type check and test / quality (pull_request) Failing after 28s
Lint, type check and test / release (pull_request) Has been skipped
b70e8c7792
Cut the bench server and sequence docstrings
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 20s
Lint, type check and test / release (pull_request) Has been skipped
821810969f
Cut the release scripts' module docstrings
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 18s
Lint, type check and test / release (pull_request) Has been skipped
c8f369c01a
Claude left a comment

Gate green (1123 passed, ruff and mypy clean). Bench JSON verified identical to base except the four intentionally dropped keys (offset, transmitted, messageType, rules), none of which static/app.js reads. Findings:

protocol/units.py sentinel_at — a bare KeyError where there was None or a named refusal. _BY_PATTERN[signed, width] raises KeyError for any shape not in the table. Old behaviour: (False, 1) and (False, 8) returned None; (True, 4) raised ValueError("the specification reserves no signed 32-bit values"). That message was a spec statement and it is gone. Not reachable today — every field measurement.measurement_of routes to interpret is width 2 or unsigned 4 — but a width-1 quantity field added to the catalogue later would raise inside frame decoding instead of returning nothing. Keep the explicit refusal for signed-32 and fall back to {} for the rest.

Related, same function: raw % (1 << width * 8) now names a sentinel for a raw value wider than the field (sentinel_at(0x1FFFF, signed=False, width=2) is UNKNOWN, was None). Also unreachable, also silent if it ever is not.

entity.py:154 — self._key is cached in __init__ but async_redescribed (line 167) reassigns self.endpoint without it. Correct today only because platforms.settle looks the entity up by key before calling it, so the new endpoint always has the same key. Nothing states or enforces that. Set _key alongside self.endpoint, or drop the cache.

**updater/firmware.py:344 _destination** — the new docstring, "scheme and host, lower-cased, as urllib does not", says both get lower-cased here. Only the host does; urllib already lower-cases the scheme, which is what the old comment said. It also dropped the reason for or ""(a URL with no//` has no host).

traffic.py:1 — the module docstring lost the reason traffic reads the identifier and never decodes a payload: a second subscriber pulling frames apart is a second chance to disagree with the codec. That is the drift risk the repository is built around and the code cannot carry it. Worth one line back.

**updater/firmware.py:79 FETCH_STOP** — lost "Nothing clears it: the next process starts with an event of its own." A module-global threading.Event` that is never reset is exactly the trap a future editor (or a test) falls into.

**updater/file_server.py:31 image_path** — lost "only the newest image per node type is offered, so the version and the environment name it unambiguously". That uniqueness is what makes offers()` safe to key on the path.

**tools/frame_bench/codec.py:154 _address** — three specific messages collapsed to one, and the parser got stricter. 192.168.1.300said "'300' is not between 0 and 255" and now says "Write this as an address, like 192.168.1.10.";192.168.001.10 used to encode and now refuses (IPv4Address` rejects leading zeros). Fine if deliberate, but it is an operator-facing change, not just a trim.

**coordinator.py:188 _availability_changed** — routing through notifymeans an exception in an availability listener is now logged rather than propagated into the transport's_announce`. Better behaviour, but it is a behaviour change in one of the thin-cover areas and no test pins it either way.

tools/frame_bench/server.py:161 — mimetypes.guess_file_type reads the host's /etc/mime.types where the old three-entry table could not vary. Right answers on this machine; a host with an odd .js mapping now breaks the page.

Nits: readings.py:52 subscribe's docstring restates the signature exactly — with D1off it can go.updater/session.py:41-43— the"""The longest name and key..."""docstring sits underMAX_KEYso it reads as documenting only the key, and duplicates the#comment aboveMAX_SSID. updater/session.py:215— a hotspot failure now logs WARNING without a traceback where it logged ERROR with one;radio.pycalls a Pi left as an access point the failure that matters, andstr(failure)` is all that is left of it.

Gate green (1123 passed, ruff and mypy clean). Bench JSON verified identical to base except the four intentionally dropped keys (`offset`, `transmitted`, `messageType`, `rules`), none of which `static/app.js` reads. Findings: **`protocol/units.py` `sentinel_at` — a bare `KeyError` where there was `None` or a named refusal.** `_BY_PATTERN[signed, width]` raises `KeyError` for any shape not in the table. Old behaviour: `(False, 1)` and `(False, 8)` returned `None`; `(True, 4)` raised `ValueError("the specification reserves no signed 32-bit values")`. That message was a spec statement and it is gone. Not reachable today — every field `measurement.measurement_of` routes to `interpret` is width 2 or unsigned 4 — but a width-1 quantity field added to the catalogue later would raise inside frame decoding instead of returning nothing. Keep the explicit refusal for signed-32 and fall back to `{}` for the rest. Related, same function: `raw % (1 << width * 8)` now names a sentinel for a raw value wider than the field (`sentinel_at(0x1FFFF, signed=False, width=2)` is `UNKNOWN`, was `None`). Also unreachable, also silent if it ever is not. **`entity.py:154` — `self._key` is cached in `__init__` but `async_redescribed` (line 167) reassigns `self.endpoint` without it.** Correct today only because `platforms.settle` looks the entity up by key before calling it, so the new endpoint always has the same key. Nothing states or enforces that. Set `_key` alongside `self.endpoint`, or drop the cache. **`updater/firmware.py:344 `_destination`** — the new docstring, "scheme and host, lower-cased, as urllib does not", says both get lower-cased here. Only the host does; urllib already lower-cases the scheme, which is what the old comment said. It also dropped the reason for `or ""` (a URL with no `//` has no host). **`traffic.py:1`** — the module docstring lost the reason traffic reads the identifier and never decodes a payload: a second subscriber pulling frames apart is a second chance to disagree with the codec. That is the drift risk the repository is built around and the code cannot carry it. Worth one line back. **`updater/firmware.py:79 `FETCH_STOP`** — lost "Nothing clears it: the next process starts with an event of its own." A module-global `threading.Event` that is never reset is exactly the trap a future editor (or a test) falls into. **`updater/file_server.py:31 `image_path`** — lost "only the newest image per node type is offered, so the version and the environment name it unambiguously". That uniqueness is what makes `offers()` safe to key on the path. **`tools/frame_bench/codec.py:154 `_address`** — three specific messages collapsed to one, and the parser got stricter. `192.168.1.300` said "'300' is not between 0 and 255" and now says "Write this as an address, like 192.168.1.10."; `192.168.001.10` used to encode and now refuses (`IPv4Address` rejects leading zeros). Fine if deliberate, but it is an operator-facing change, not just a trim. **`coordinator.py:188 `_availability_changed`** — routing through `notify` means an exception in an availability listener is now logged rather than propagated into the transport's `_announce`. Better behaviour, but it is a behaviour change in one of the thin-cover areas and no test pins it either way. **`tools/frame_bench/server.py:161`** — `mimetypes.guess_file_type` reads the host's `/etc/mime.types` where the old three-entry table could not vary. Right answers on this machine; a host with an odd `.js` mapping now breaks the page. Nits: `readings.py:52 `subscribe`'s docstring restates the signature exactly — with `D1` off it can go. `updater/session.py:41-43` — the `"""The longest name and key..."""` docstring sits under `MAX_KEY` so it reads as documenting only the key, and duplicates the `#` comment above `MAX_SSID`. `updater/session.py:215` — a hotspot failure now logs WARNING without a traceback where it logged ERROR with one; `radio.py` calls a Pi left as an access point the failure that matters, and `str(failure)` is all that is left of it.
Keep sentinel_at answering for every field shape
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 10s
Lint, type check and test / quality (pull_request) Failing after 20s
Lint, type check and test / release (pull_request) Has been skipped
6ed11df976
Follow the endpoint key when an endpoint is redescribed
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 9s
Lint, type check and test / quality (pull_request) Failing after 21s
Lint, type check and test / release (pull_request) Has been skipped
74d46b9892
Put back three reasons the trim should have kept
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 14s
Lint, type check and test / quality (pull_request) Failing after 20s
Lint, type check and test / release (pull_request) Has been skipped
99c7e4788b
Drop two docstrings that said nothing new
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 28s
Lint, type check and test / quality (pull_request) Failing after 29s
Lint, type check and test / release (pull_request) Has been skipped
692b20c032
Author
Collaborator

Taken the review. Four fixes pushed:

  • sentinel_at answers for every field shape again, and no longer wraps a raw value wider than its field. The signed-32-bit ValueError is back, spec statement and all (6ed11df).
  • async_redescribed now refreshes _key with the endpoint it just took (74d46b9).
  • _destination's docstring says what the code does: urllib lower-cases the scheme, not the host (99c7e47).
  • The three lost reasons are back, one or two lines each: why traffic.py decodes no payload, that nothing clears FETCH_STOP, and that only the newest image per node is offered so image_path is unique (99c7e47).

Two nits also taken: readings.subscribe's docstring and the MAX_KEY docstring that was attached to the wrong name (692b20c).

Two I am not changing:

The address parser. ipaddress.IPv4Address refusing 192.168.001.10 is deliberate upstream behaviour, not an accident — leading zeros are ambiguous between decimal and octal, and every parser that guessed has been a source of SSRF bugs. The bench is where a frame is typed by hand before it goes on a real van's bus, so refusing the ambiguous form and asking for 192.168.1.10 is the answer I want. The cost is real and I have noted it: three specific messages became one, so it no longer says which octet is wrong.

Static content types. mimetypes carries .html, .css and .js in its own built-in table; /etc/mime.types only adds to it. A host without that file still serves the three files this directory holds correctly — verified against a running bench, all three headers byte-identical to what the old dict produced, charset included.

One thing outside this branch: feat/firmware-updates has moved on since the branch was cut (#78), and the PR now conflicts in six files. I have deliberately left that alone — resolving it means merging a feature branch's behaviour into a refactor branch, which is your call rather than mine.

Taken the review. Four fixes pushed: - `sentinel_at` answers for every field shape again, and no longer wraps a raw value wider than its field. The signed-32-bit `ValueError` is back, spec statement and all (6ed11df). - `async_redescribed` now refreshes `_key` with the endpoint it just took (74d46b9). - `_destination`'s docstring says what the code does: urllib lower-cases the scheme, not the host (99c7e47). - The three lost reasons are back, one or two lines each: why `traffic.py` decodes no payload, that nothing clears `FETCH_STOP`, and that only the newest image per node is offered so `image_path` is unique (99c7e47). Two nits also taken: `readings.subscribe`'s docstring and the `MAX_KEY` docstring that was attached to the wrong name (692b20c). Two I am not changing: **The address parser.** `ipaddress.IPv4Address` refusing `192.168.001.10` is deliberate upstream behaviour, not an accident — leading zeros are ambiguous between decimal and octal, and every parser that guessed has been a source of SSRF bugs. The bench is where a frame is typed by hand before it goes on a real van's bus, so refusing the ambiguous form and asking for `192.168.1.10` is the answer I want. The cost is real and I have noted it: three specific messages became one, so it no longer says which octet is wrong. **Static content types.** `mimetypes` carries `.html`, `.css` and `.js` in its own built-in table; `/etc/mime.types` only adds to it. A host without that file still serves the three files this directory holds correctly — verified against a running bench, all three headers byte-identical to what the old dict produced, charset included. One thing outside this branch: `feat/firmware-updates` has moved on since the branch was cut (#78), and the PR now conflicts in six files. I have deliberately left that alone — resolving it means merging a feature branch's behaviour into a refactor branch, which is your call rather than mine.
Claude left a comment

Second round. The four fixes are good: sentinel_at is byte-identical to the base for every (signed, width, raw) I could throw at it including negatives, oversized raws and widths 1/3/8; _key now follows a redescribe and binary_sensor's alarm test at line 98 rides on it. The bench's rendered catalogue JSON is identical to the base for every message except the four keys deliberately dropped (messageType, rules, offset, transmitted), and app.js reads none of them. Built frames, saved examples, and every message's priority and field layout come out identical.

New findings, worst first.

tools/frame_bench/protocol.py:1320 — a spec citation that did not exist before. # Section 13 lifts a hold above the other commands. Section 13 is cited nowhere on feat/firmware-updates and nowhere else in this repo; the base just had priority=1 with no comment. If that came from docs/can-protocol.md, fine. If it was reasoned back from the number, it is inferring the protocol from the code and will read as authoritative to the next person. Confirm or drop it.

pyproject.toml:17 — ignore = ["D1", ...] is wider than the change needed. D1 is the whole missing-docstring family, so D100 (module), D101 (class) and D104 (package) are off as well. A new module or class can now land with no docstring at all. ignore = ["D102", "D103", "D105", "D107", "D206", "E101", "W191"] passes on this tree as it stands — I ran it — and keeps the three that matter.

custom_components/campervan/firmware.py:219 — an empty refusal now raises an empty error. The base was _refusal(payload) or unexplained, so {"error": ""} fell through to the updater answered POST /x with 500. refusal if isinstance(refusal, str) else unexplained returns the empty string instead. Use refusal or unexplained after the isinstance narrowing.

tools/frame_bench/protocol.py:69-70 — offset and width gained defaults on FrameField. Both were required on the base and the fields are now positional, so a transcription that omits an offset silently encodes at byte 0 rather than raising TypeError. In the one file whose job is to not drift from the specification that is the wrong default. Drop = 0 from the base offset and put offset: int = 0 back on QuantityField, which is the only declaration site that omits it.

Nits:

  • custom_components/campervan/readings.py:52 — notify collapses four messages ("An alarm listener would not run", "A node listener…", "A reading listener…", "A traffic listener…") into one, logged under campervan.readings rather than the store's own module. Take the subject as an argument, or log from the caller.
  • updater/session.py:215 — folding RadioError into the BusError branch demotes "no hotspot" from _LOGGER.exception to a warning with no traceback. The SessionResult is unchanged, so this is diagnostics only, but a hotspot that will not come up is the failure worth a traceback on a van with no screen.
  • updater/file_server.py:73 — the offers parameter of manifest_body shadows the module-level offers() nine lines above. Rename it advertised.
  • custom_components/campervan/lockout.py:41 — ends + timedelta(microseconds=-ends.microsecond % 1_000_000) is correct (checked against the old form over 20k random times) but the modular-negation trick needs a second read where the three lines it replaced did not.
Second round. The four fixes are good: `sentinel_at` is byte-identical to the base for every `(signed, width, raw)` I could throw at it including negatives, oversized raws and widths 1/3/8; `_key` now follows a redescribe and `binary_sensor`'s alarm test at line 98 rides on it. The bench's rendered catalogue JSON is identical to the base for every message except the four keys deliberately dropped (`messageType`, `rules`, `offset`, `transmitted`), and `app.js` reads none of them. Built frames, saved examples, and every message's priority and field layout come out identical. New findings, worst first. **`tools/frame_bench/protocol.py:1320` — a spec citation that did not exist before.** `# Section 13 lifts a hold above the other commands.` Section 13 is cited nowhere on `feat/firmware-updates` and nowhere else in this repo; the base just had `priority=1` with no comment. If that came from `docs/can-protocol.md`, fine. If it was reasoned back from the number, it is inferring the protocol from the code and will read as authoritative to the next person. Confirm or drop it. **`pyproject.toml:17` — `ignore = ["D1", ...]` is wider than the change needed.** `D1` is the whole missing-docstring family, so D100 (module), D101 (class) and D104 (package) are off as well. A new module or class can now land with no docstring at all. `ignore = ["D102", "D103", "D105", "D107", "D206", "E101", "W191"]` passes on this tree as it stands — I ran it — and keeps the three that matter. **`custom_components/campervan/firmware.py:219` — an empty refusal now raises an empty error.** The base was `_refusal(payload) or unexplained`, so `{"error": ""}` fell through to `the updater answered POST /x with 500`. `refusal if isinstance(refusal, str) else unexplained` returns the empty string instead. Use `refusal or unexplained` after the isinstance narrowing. **`tools/frame_bench/protocol.py:69-70` — `offset` and `width` gained defaults on `FrameField`.** Both were required on the base and the fields are now positional, so a transcription that omits an offset silently encodes at byte 0 rather than raising `TypeError`. In the one file whose job is to not drift from the specification that is the wrong default. Drop `= 0` from the base `offset` and put `offset: int = 0` back on `QuantityField`, which is the only declaration site that omits it. Nits: - `custom_components/campervan/readings.py:52` — `notify` collapses four messages ("An alarm listener would not run", "A node listener…", "A reading listener…", "A traffic listener…") into one, logged under `campervan.readings` rather than the store's own module. Take the subject as an argument, or log from the caller. - `updater/session.py:215` — folding `RadioError` into the `BusError` branch demotes "no hotspot" from `_LOGGER.exception` to a `warning` with no traceback. The `SessionResult` is unchanged, so this is diagnostics only, but a hotspot that will not come up is the failure worth a traceback on a van with no screen. - `updater/file_server.py:73` — the `offers` parameter of `manifest_body` shadows the module-level `offers()` nine lines above. Rename it `advertised`. - `custom_components/campervan/lockout.py:41` — `ends + timedelta(microseconds=-ends.microsecond % 1_000_000)` is correct (checked against the old form over 20k random times) but the modular-negation trick needs a second read where the three lines it replaced did not.
Cite the section that actually raises a hold
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 9s
Lint, type check and test / quality (pull_request) Failing after 22s
Lint, type check and test / release (pull_request) Has been skipped
b042b9adbb
Still require a docstring on a module and a class
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 21s
Lint, type check and test / release (pull_request) Has been skipped
ce33dbcde6
Explain a blank refusal rather than raise it
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 12s
Lint, type check and test / quality (pull_request) Failing after 21s
Lint, type check and test / release (pull_request) Has been skipped
bbe970220f
Make a bench field state its offset
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 26s
Lint, type check and test / quality (pull_request) Failing after 31s
Lint, type check and test / release (pull_request) Has been skipped
adc2c46d67
Name the manifest argument and the round-up
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 18s
Lint, type check and test / release (pull_request) Has been skipped
2c9729d131
Author
Collaborator

Round two taken. Six commits:

  • The section 13 citation was wrong. I had reasoned it back from tests/test_frame_bench.py:71. The specification puts CMD_HOLD at priority 1 in its §8.3 catalogue entry, not in §13, so the comment now says §8.3 (b042b9a). Checked against docs/can-protocol.md in the firmware repository, line 564.
  • ignore is now ["D102", "D103", "D105", "D107", …] rather than the whole of D1, so a new module or class still has to carry a docstring (ce33dbc).
  • An empty {"error": ""} falls back to the status line again, as _refusal(…) or unexplained did (bbe9702).
  • FrameField.offset is required again. Only the one QuantityField legitimately sat at zero, and it now says so (adc2c46). The rendered catalogue JSON is unchanged.
  • The manifest argument is advertised, so it no longer shadows offers(), and the lockout round-up gets a name (2c9729d).

Two nits declined:

readings.notify logging under one logger. That is what folding six copies into one helper costs. Every one of the six said the same thing and no test reads the text; the stack in the traceback still names the listener. Splitting it back apart to keep six module loggers would undo the change.

session.run's hotspot failure at WARNING without a traceback. Deliberate and already listed in the description. A hotspot that will not come up is an operational fact, not a bug in the updater, and the failure's own message says which step failed.

Round two taken. Six commits: - **The section 13 citation was wrong.** I had reasoned it back from `tests/test_frame_bench.py:71`. The specification puts `CMD_HOLD` at priority 1 in its §8.3 catalogue entry, not in §13, so the comment now says §8.3 (b042b9a). Checked against `docs/can-protocol.md` in the firmware repository, line 564. - `ignore` is now `["D102", "D103", "D105", "D107", …]` rather than the whole of `D1`, so a new module or class still has to carry a docstring (ce33dbc). - An empty `{"error": ""}` falls back to the status line again, as `_refusal(…) or unexplained` did (bbe9702). - `FrameField.offset` is required again. Only the one `QuantityField` legitimately sat at zero, and it now says so (adc2c46). The rendered catalogue JSON is unchanged. - The manifest argument is `advertised`, so it no longer shadows `offers()`, and the lockout round-up gets a name (2c9729d). Two nits declined: **`readings.notify` logging under one logger.** That is what folding six copies into one helper costs. Every one of the six said the same thing and no test reads the text; the stack in the traceback still names the listener. Splitting it back apart to keep six module loggers would undo the change. **`session.run`'s hotspot failure at WARNING without a traceback.** Deliberate and already listed in the description. A hotspot that will not come up is an operational fact, not a bug in the updater, and the failure's own message says which step failed.
Claude left a comment

Round three. The six fixes are correct; §8.3 checks out against docs/can-protocol.md:564, and the bench's rendered catalogue JSON is a strict subset of the base branch's (only offset, transmitted, messageType and rules dropped, not one value changed). Nothing blocking. Three wording points, the first of which is a comment that now says the wrong thing.

Round three. The six fixes are correct; §8.3 checks out against `docs/can-protocol.md:564`, and the bench's rendered catalogue JSON is a strict subset of the base branch's (only `offset`, `transmitted`, `messageType` and `rules` dropped, not one value changed). Nothing blocking. Three wording points, the first of which is a comment that now says the wrong thing.
@ -99,4 +87,1 @@
Only `cleared` clears it. Reading a state this build has no word for
as off is the one mistake a gas alarm may not make.
"""
return self.state != AlarmState.CLEARED
Author
Collaborator

Nit. The reason for != rather than a membership test went with the docstring — reading a state this build has no word for as off is the one mistake a gas alarm may not make. The module docstring still carries the rule, but the invariant is now four screens from the line that depends on it, and self.state in (RAISED, ACTIVE) looks like a tidy-up. One trailing comment would hold it.

Nit. The reason for `!=` rather than a membership test went with the docstring — reading a state this build has no word for as off is the one mistake a gas alarm may not make. The module docstring still carries the rule, but the invariant is now four screens from the line that depends on it, and `self.state in (RAISED, ACTIVE)` looks like a tidy-up. One trailing comment would hold it.
@ -207,3 +161,3 @@
@callback
def _frame_arrived(self, message: can.Message) -> None:
"""Hand one frame to everyone listening.
"""Hand one frame to everyone listening, counting it once if none could.
Author
Collaborator

"counting it once if none could" inverts the rule. unusable is set by any listener raising, so the frame is counted once however many listeners could not use it — not only when none could. The old wording had it right; the summary just needs to say "however many listeners could not".

"counting it once if none could" inverts the rule. `unusable` is set by *any* listener raising, so the frame is counted once however many listeners could not use it — not only when none could. The old wording had it right; the summary just needs to say "however many listeners could not".
@ -10,3 +7,1 @@
none and has no descriptors to serve; a node asking `QRY_DESCRIPTORS` gets
nothing back from here, which spec section 8.7 says it should and nothing here
does yet.
It owns no endpoints, so it serves no descriptors, which spec section 8.7 says
Author
Collaborator

The "which" now attaches to "serves no descriptors", so it reads as though §8.7 endorses serving none. The point was the opposite: a node asking QRY_DESCRIPTORS gets nothing back from here, which §8.7 says it should get. Naming QRY_DESCRIPTORS puts it back in one line.

The "which" now attaches to "serves no descriptors", so it reads as though §8.7 endorses serving none. The point was the opposite: a node asking `QRY_DESCRIPTORS` gets nothing back from here, which §8.7 says it should get. Naming `QRY_DESCRIPTORS` puts it back in one line.
Say what three comments meant to say
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 18s
Lint, type check and test / release (pull_request) Has been skipped
bac9afe328
Author
Collaborator

All three taken in bac9afe:

  • _frame_arrived says "counting it once if any could not", which is what the code does.
  • presence.py says what §8.7 is actually about: a node asking QRY_DESCRIPTORS gets nothing back from here.
  • Alarm.is_raised has its reason back, on the line that depends on it: only cleared clears it, and reading an unnamed state as off is the one mistake a gas alarm may not make.

Gate green. Three rounds, nothing left outstanding on my side.

All three taken in bac9afe: - `_frame_arrived` says "counting it once if **any** could not", which is what the code does. - `presence.py` says what §8.7 is actually about: a node asking `QRY_DESCRIPTORS` gets nothing back from here. - `Alarm.is_raised` has its reason back, on the line that depends on it: only `cleared` clears it, and reading an unnamed state as off is the one mistake a gas alarm may not make. Gate green. Three rounds, nothing left outstanding on my side.
Trim the prose the CalVer decode arrived with
All checks were successful
Lint, type check and test / hassfest (pull_request) Successful in 26s
Lint, type check and test / quality (pull_request) Successful in 4m58s
Lint, type check and test / release (pull_request) Has been skipped
b21af58599
Claude left a comment

Reviewed the merge (dfd0bbc) and the trim that followed (b21af58). Nothing to act on.

Verified: with docstrings and comments stripped, the code delta the merge adds is identical to #78's own, across all eight touched source files — the only difference is the deliberate except (RadioError, BusError) flatten in updater/session.py. Tests, vectors and README are byte-identical to #78's tip, and the branch never touched tests. No trimmed docstring still describes the pre-#78 model; no uint16-cannot-be-ordered prose survives anywhere. Gate re-run clean: ruff, format, mypy strict, 1164 passed.

Reviewed the merge (dfd0bbc) and the trim that followed (b21af58). Nothing to act on. Verified: with docstrings and comments stripped, the code delta the merge adds is identical to #78's own, across all eight touched source files — the only difference is the deliberate `except (RadioError, BusError)` flatten in `updater/session.py`. Tests, vectors and README are byte-identical to #78's tip, and the branch never touched tests. No trimmed docstring still describes the pre-#78 model; no `uint16`-cannot-be-ordered prose survives anywhere. Gate re-run clean: ruff, format, mypy strict, 1164 passed.
rob merged commit 2d37c0cc60 into feat/firmware-updates 2026-09-21 05:48:34 +00:00
rob deleted branch refactor/trim-verbosity 2026-09-21 05:48:34 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rob/CampervanHomeAssistant!79
No description provided.