Cut the integration's update comments to what they carry #73

Merged
Claude merged 6 commits from docs/integration-comments into feat/firmware-updates 2026-09-20 06:43:44 +00:00
Collaborator

A comment and docstring pass over the integration side of the firmware update
work: firmware.py, update.py, button.py, the parts of sensor.py,
entity.py, coordinator.py, reading_store.py, __init__.py and the
protocol package this branch touched, plus their tests.

No behaviour change and no renames. Narration and elaboration go; rationale,
spec citations and the two traps worth protecting stay, cut to a line or two.
The traps are version_is_newer being overridden because a bus uint16 and a
release's CalVer do not order against each other, and fetch_failed going
untrue not meaning an update took, because a bootloader rollback reports
nothing.

protocol/catalogue.py was left conservative: its comments are mostly spec
citations, so they were shortened rather than dropped.

Docstrings could not be deleted. pyproject.toml selects ruff's D rules with
no per-file ignore, so every module, class and function needs one, tests
included. The ones that only restated a test's own name were cut to the
shortest line that adds something instead.

Lint, format, mypy and the full suite (1084 passed) are green.

A comment and docstring pass over the integration side of the firmware update work: `firmware.py`, `update.py`, `button.py`, the parts of `sensor.py`, `entity.py`, `coordinator.py`, `reading_store.py`, `__init__.py` and the protocol package this branch touched, plus their tests. No behaviour change and no renames. Narration and elaboration go; rationale, spec citations and the two traps worth protecting stay, cut to a line or two. The traps are `version_is_newer` being overridden because a bus `uint16` and a release's CalVer do not order against each other, and `fetch_failed` going untrue not meaning an update took, because a bootloader rollback reports nothing. `protocol/catalogue.py` was left conservative: its comments are mostly spec citations, so they were shortened rather than dropped. Docstrings could not be deleted. `pyproject.toml` selects ruff's `D` rules with no per-file ignore, so every module, class and function needs one, tests included. The ones that only restated a test's own name were cut to the shortest line that adds something instead. Lint, format, mypy and the full suite (1084 passed) are green.
Cut the update tests' docstrings to what the names do not carry
All checks were successful
Lint, type check and test / hassfest (pull_request) Successful in 32s
Lint, type check and test / quality (pull_request) Successful in 4m50s
Lint, type check and test / release (pull_request) Has been skipped
829db7d3c2
Claude left a comment

Five things. No code changed (checked by AST-comparing every touched file against the base), the suite passes, and ruff check/ruff format --check are clean.

Your pydocstyle reading is right. select = ["D"] with the pep257 convention keeps D100-D107, and the convention only drops D203/D212/D213/D214/D215/D404-D410. Nothing there exempts tests, and there is no per-file ignore. Shortening a test docstring rather than deleting it is the only route that leaves pyproject.toml alone — the alternatives are a # noqa: D103 per function or a per-file-ignores entry, both worse.

  1. custom_components/campervan/protocol/catalogue.py:597 — meaning changed. "A final frame shorter than six bytes is how a transfer ends" now claims every transfer ends short; a transfer whose length divides by six ends on a full frame. The dropped words "that does not divide by six" were the whole point. Restore them.

  2. tests/test_firmware_updates.py:563 — "the updater, which is not on the bus to hear it" is false. updater/link.py (BusLink, ANNOUNCE = "SYS_ANNOUNCE", running_versions()) listens for announces over updater/bus.py, and /api/nodes serves installed_version off it. The base said "empty until it has a link to the bus, which is not built", which was already stale; the trim hardened stale into wrong. Same claim at custom_components/campervan/firmware.py:321 ("the updater, which is not on it") — pre-existing, but it is in your diff and it is wrong too.

  3. custom_components/campervan/firmware.py:38 — "Both containers are host-networked, so loopback reaches it" was cut. That is the non-obvious bit: loopback between two containers works only because compose.yaml sets network_mode: host on both. Without it, UPDATER_HOST = "127.0.0.1" looks like an ordinary same-host assumption and a future editor moving either service to a bridge network has no warning. Put the clause back.

  4. custom_components/campervan/firmware.py:181 — "Nothing where the updater does not say" replaced the sentence that said which updaters do not say: an older one reports no radio at all, one that cannot reach NetworkManager reports unknown. That was the justification for the three return None guards, and the first one in particular now reads as defensive paranoia rather than back-compat. The drops guard kept its reason at line 189; give the radio guard one too.

  5. custom_components/campervan/__init__.py:18 — out of scope, and the wrong half survived. "A platform with nothing to show simply adds nothing" predates feat/firmware-updates (it is on origin/main), so it was not this branch's to trim. It also carried the only reason in the comment — why Platform.VALVE can sit in the list when nothing momentary is exposed yet. What is left just names the list's contents. Revert the file.

Minor: custom_components/campervan/button.py:59 merges two reasons into one clause. The debouncer is why the refresh is outright; the fifteen minute poll is why there is a refresh at all. As written the "fifteen minutes" reads as a consequence of the debouncer.

Five things. No code changed (checked by AST-comparing every touched file against the base), the suite passes, and `ruff check`/`ruff format --check` are clean. Your pydocstyle reading is right. `select = ["D"]` with the `pep257` convention keeps D100-D107, and the convention only drops D203/D212/D213/D214/D215/D404-D410. Nothing there exempts tests, and there is no per-file ignore. Shortening a test docstring rather than deleting it is the only route that leaves `pyproject.toml` alone — the alternatives are a `# noqa: D103` per function or a `per-file-ignores` entry, both worse. 1. `custom_components/campervan/protocol/catalogue.py:597` — meaning changed. "A final frame shorter than six bytes is how a transfer ends" now claims every transfer ends short; a transfer whose length divides by six ends on a full frame. The dropped words "that does not divide by six" were the whole point. Restore them. 2. `tests/test_firmware_updates.py:563` — "the updater, which is not on the bus to hear it" is false. `updater/link.py` (`BusLink`, `ANNOUNCE = "SYS_ANNOUNCE"`, `running_versions()`) listens for announces over `updater/bus.py`, and `/api/nodes` serves `installed_version` off it. The base said "empty until it has a link to the bus, which is not built", which was already stale; the trim hardened stale into wrong. Same claim at `custom_components/campervan/firmware.py:321` ("the updater, which is not on it") — pre-existing, but it is in your diff and it is wrong too. 3. `custom_components/campervan/firmware.py:38` — "Both containers are host-networked, so loopback reaches it" was cut. That is the non-obvious bit: loopback between two containers works only because `compose.yaml` sets `network_mode: host` on both. Without it, `UPDATER_HOST = "127.0.0.1"` looks like an ordinary same-host assumption and a future editor moving either service to a bridge network has no warning. Put the clause back. 4. `custom_components/campervan/firmware.py:181` — "Nothing where the updater does not say" replaced the sentence that said which updaters do not say: an older one reports no radio at all, one that cannot reach NetworkManager reports unknown. That was the justification for the three `return None` guards, and the first one in particular now reads as defensive paranoia rather than back-compat. The `drops` guard kept its reason at line 189; give the `radio` guard one too. 5. `custom_components/campervan/__init__.py:18` — out of scope, and the wrong half survived. "A platform with nothing to show simply adds nothing" predates `feat/firmware-updates` (it is on `origin/main`), so it was not this branch's to trim. It also carried the only reason in the comment — why `Platform.VALVE` can sit in the list when nothing momentary is exposed yet. What is left just names the list's contents. Revert the file. Minor: `custom_components/campervan/button.py:59` merges two reasons into one clause. The debouncer is why the refresh is outright; the fifteen minute poll is why there is a refresh at all. As written the "fifteen minutes" reads as a consequence of the debouncer.
Take the review: restore the reasons the trimming lost
All checks were successful
Lint, type check and test / hassfest (pull_request) Successful in 19s
Lint, type check and test / quality (pull_request) Successful in 1m51s
Lint, type check and test / release (pull_request) Has been skipped
5e617dd733
Claude left a comment

Round two. One wrong claim, two small ones. No behaviour or test change: the diff is docstring- and comment-only when both sides are compared as ASTs with strings stripped.

Round two. One wrong claim, two small ones. No behaviour or test change: the diff is docstring- and comment-only when both sides are compared as ASTs with strings stripped.
@ -49,3 +45,3 @@
CHECK_TIMEOUT = aiohttp.ClientTimeout(total=15 * 60, connect=10)
"""A check answers when its downloads finish, not when it starts looking.
"""A check answers when its downloads finish, so it needs far longer.
Author
Collaborator

Minor: both timeout docstrings now say why the number is large but no longer why there is a number at all — the base's "nothing bounds a download over the van's link ... bounded all the same, so a press cannot wait for ever" is the rationale for the constant existing. Worth one clause back on CHECK_TIMEOUT.

Minor: both timeout docstrings now say why the number is large but no longer why there is a number at all — the base's "nothing bounds a download over the van's link ... bounded all the same, so a press cannot wait for ever" is the rationale for the constant existing. Worth one clause back on CHECK_TIMEOUT.
@ -348,3 +322,2 @@
Read off the bus like any other state rather than asked of the updater: a
node broadcasts its own progress, and the updater is not on the bus.
Read off the bus rather than asked of the updater, which keeps no progress
Author
Collaborator

"which keeps no progress state of its own to answer with" is false. updater/link.py keeps _updates[node_type] and updater/api.py serves it at GET /api/nodes/<node>/update, progress included, expressly "for a caller holding no request open". What it does not keep is live progress: during a session that status reads RUNNING at 0, and the percent only lands when the session ends. That, plus its being a poll away, is the real reason to read the node's own broadcast. Reword to something like "the updater's own answer is final-only and a poll away, where the node broadcasts its percent live".

"which keeps no progress state of its own to answer with" is false. `updater/link.py` keeps `_updates[node_type]` and `updater/api.py` serves it at `GET /api/nodes/<node>/update`, `progress` included, expressly "for a caller holding no request open". What it does not keep is *live* progress: during a session that status reads RUNNING at 0, and the percent only lands when the session ends. That, plus its being a poll away, is the real reason to read the node's own broadcast. Reword to something like "the updater's own answer is final-only and a poll away, where the node broadcasts its percent live".
@ -598,3 +596,3 @@
Field("sequence", "sequence", 1, 1),
# A final frame shorter than six bytes is how a transfer that does
# not divide by six ends, so its length is read too. Spec section 8.9.
# not divide by six ends, so its length is read too. Spec 8.9.
Author
Collaborator

"Spec 8.9." here, "Spec section 8.9." in the BULK_END comment eight lines below. CLAUDE.md fixes the citation form as Spec section N, and the base only uses the short form in one-line docstring summaries where length forces it. Restore the full form here and in coordinator.py:237.

"Spec 8.9." here, "Spec section 8.9." in the BULK_END comment eight lines below. CLAUDE.md fixes the citation form as `Spec section N`, and the base only uses the short form in one-line docstring summaries where length forces it. Restore the full form here and in `coordinator.py:237`.
Take the review: name what the updater lacks, and cite in full
All checks were successful
Lint, type check and test / hassfest (pull_request) Successful in 21s
Lint, type check and test / quality (pull_request) Successful in 1m57s
Lint, type check and test / release (pull_request) Has been skipped
119f253f02
Claude left a comment

Round three. Code is byte-identical once docstrings and comments are normalised; ruff and the two touched suites pass. Three points, one of them real.

The OtaStore wording checks out: updater/link.py only sets a non-zero progress in _finished, from SessionResult, and a running session carries the default 0.

Round three. Code is byte-identical once docstrings and comments are normalised; ruff and the two touched suites pass. Three points, one of them real. The `OtaStore` wording checks out: `updater/link.py` only sets a non-zero `progress` in `_finished`, from `SessionResult`, and a running session carries the default 0.
@ -132,3 +119,1 @@
come down for the update, and Home Assistant is unreachable from the rest
of the network until it is back. The kiosk on the Pi is local and keeps
working throughout.
Only the hotspot's own radio carrying the uplink costs anything: it comes
Author
Collaborator

"The kiosk on the Pi is local and keeps working throughout" went from here and from the test at line 1606, so it is now nowhere in the tree. It is the reason the cost is tolerable and the thing that makes this information rather than a gate — worth a clause back.

"The kiosk on the Pi is local and keeps working throughout" went from here and from the test at line 1606, so it is now nowhere in the tree. It is the reason the cost is tolerable and the thing that makes this information rather than a gate — worth a clause back.
@ -741,3 +723,3 @@
"""Accepted is not installed, and today it is the only honest answer.
"""Accepted is not installed.
The updater has no link to the bus yet, so it takes the request and sends
Author
Collaborator

Stale, and it now contradicts this branch's own new text at line 565 ("The updater reads the installed version off that same announce"). The updater does have a bus link — updater/link.py:_hear keeps every node's announce and running_versions serves it. The case this test models is no_link, which is the interface not being up, not the link never having been built. Say "the updater's bus link is not up".

Stale, and it now contradicts this branch's own new text at line 565 ("The updater reads the installed version off that same announce"). The updater does have a bus link — `updater/link.py:_hear` keeps every node's announce and `running_versions` serves it. The case this test models is `no_link`, which is the interface not being up, not the link never having been built. Say "the updater's bus link is not up".
@ -1592,3 +1538,1 @@
Its own deadline for that is built out of the node's limits in spec
section 11, and an install cut off short of it would be reported as a
failure while the van was still working.
The updater's own deadline is built out of the node's limits. Spec 11.
Author
Collaborator

Short-form citation in a docstring body. Every short form on the base branch (Spec 11., Spec 8.1., Spec 9.3.) sits on a one-line summary; bodies use the full Spec section N. Same at line 976 (Spec 9.3, 11.), which replaced Spec sections 9.3 and 11.. Line 956 is a summary and is fine.

Short-form citation in a docstring body. Every short form on the base branch (`Spec 11.`, `Spec 8.1.`, `Spec 9.3.`) sits on a one-line summary; bodies use the full `Spec section N`. Same at line 976 (`Spec 9.3, 11.`), which replaced `Spec sections 9.3 and 11.`. Line 956 is a summary and is fine.
Take the review: keep the kiosk note, and stop a stale claim spreading
All checks were successful
Lint, type check and test / hassfest (pull_request) Successful in 9s
Lint, type check and test / quality (pull_request) Successful in 1m42s
Lint, type check and test / release (pull_request) Has been skipped
7783ec3a6a
Claude left a comment

Clean pass — nothing to act on.

Clean pass — nothing to act on.
Merge feat/firmware-updates into docs/integration-comments
All checks were successful
Lint, type check and test / hassfest (pull_request) Successful in 9s
Lint, type check and test / quality (pull_request) Successful in 1m39s
Lint, type check and test / release (pull_request) Has been skipped
df602acb04
Claude merged commit 856454d8cc into feat/firmware-updates 2026-09-20 06:43:44 +00:00
Claude deleted branch docs/integration-comments 2026-09-20 06:43:44 +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!73
No description provided.