Repository navigation
Merge from upstream - #2
Merged
Merged
Conversation
…ktree `extraPaths` named `span-panel-api-security`, a scratch worktree created for the security work and merged since. Two consequences, one of which was visible all along: Pyright resolved `span_panel_api` through both that path and the installed distribution, so every symbol crossing the boundary reported as two incompatible types with the same name -- `models.V2HomieSchema` is not assignable to `span_panel_api.models.V2HomieSchema`. mypy, which reads the installed distribution alone, was clean throughout. The second is that the path was about to break: the worktree is being removed now that its branch is merged. `[tool.uv.sources]` above already names the real checkout; this line was the one left behind.
span-panel-api now reports a meter value the panel has not sent as None rather than fabricating 0.0, because on a cumulative counter the two are not interchangeable — a fabricated zero is indistinguishable from the reset the dip compensator exists to absorb (#259). The four derived value functions flattened that distinction straight back out. Net energy, at circuit and panel level, was `(a or 0) - (b or 0)`, so a circuit that had reported nothing answered a confident 0 Wh; those are TOTAL, not TOTAL_INCREASING, so the cost is a false dip to zero in history rather than corrupted statistics, but it is the same invented reading one layer further out and past the point where anything downstream can tell. Circuit power failed the other way: `-c.instant_power_w` raises TypeError on None, caught upstream and logged as a value-function failure for every PV circuit on every replay. Both now compose through helpers that propagate None, which reaches _process_raw_value and renders the sensor unknown. The panel pair were the worst exposed: both lugs devices are told apart by one `direction` property, so until it arrives neither role resolves and all four inputs are absent together. Test factories widen to float | None to match the snapshot model.
Dip compensation booked an offset the moment a counter read lower than it had, on the evidence of one sample, and that write is irreversible: it lands in long-term statistics, where undoing it is a manual adjustment per affected hour. #259 is what a wrong sample costs there — a transient zero booking a circuit's whole lifetime counter, every restart, reaching 8.18 MWh on one circuit. A firmware counter reset is permanent: the counter restarts low and counts up from there. So a reading that drops and then returns was never a reset. Compensation is still applied the moment the drop is seen, because the sensor has to stay correct meanwhile, but the conclusion is held open: retract raw >= baseline - 1.0 the counter came back confirm dipped_raw < raw < baseline-1.0 it is counting from the lower base hold raw == dipped_raw no evidence either way Confirmation needs independent progress rather than a second low reading. Snapshots dispatch on a debounce, so one replay can deliver the same incomplete state twice; a rule satisfied by any low reading would be satisfied by a second copy of the sample it exists to reject. The notification moves from detection to confirmation. Firing on sight is what made #259 so loud — a notification naming essentially every circuit on the panel for an event that had not happened. A retracted dip now produces none, because nothing happened. The pending record persists, because a restart is exactly when one is most likely to be outstanding: the reading that settles a dip is the one after the reading that booked it, and HA can stop in between. Forgetting it was provisional would make the restart itself the thing that makes a bad offset permanent. Two existing tests pinned the old behaviour and now state the new: a counter that came back used to leave its offset behind for good, and the coordinator was told on detection. Cost: a genuine reset on a circuit drawing nothing stays unconfirmed until it next moves. Only the notification waits; compensation applies throughout.
… time `3cbf02a` pointed `[tool.uv.sources]` and `[tool.pyright].extraPaths` at a scratch worktree and committed it. It stood until `82a512f` -- several commits and hours of other work -- and while it stood a test fixture was vendored through that resolution from a checkout that still recorded a producer defect the library had already fixed. Every version-based check passed, because the stale worktree declared the same version number as the corrected code. Only a byte comparison caught it. A version string does not identify content. Only a filesystem location does. Three layers, so the defect cannot repeat. `tests/test_library_resolution.py` reads `span_panel_api.__file__` and compares it against what `pyproject.toml` declares and what `manifest.json` pins -- neither written down here, both derived. The pinned version out of the declared location passes. That location at another version fails everywhere: nothing else can see it, because every copy of the pin agrees with the checkout's own declaration. A deliberate editable override skips locally, naming the path and version it found, and fails under `CI`, where the workflow deletes `[tool.uv.sources]` and no legitimate override exists. The fourth case is the dangerous one. A worktree on `PYTHONPATH` or `MYPYPATH` has no distribution metadata, so `importlib.metadata` keeps describing the installed distribution -- right name, right version, right location -- while the code that runs comes from somewhere else. Every version check passes on facts about a copy that is not running. Classified before the version is compared, because in that state the comparison is not about the code under test. `developer.md` documents the alternative to editing a committed file: install the worktree into the venv, `uv sync` to undo it. The root cause was that working against unreleased library code required editing a shared file; it does not any more. `scripts/check-library-path.py` rejects a `pyproject.toml` whose span-panel-api paths name anything but `../../span/span-panel-api`, and says how to redirect the library without touching the file. Both blocks, not just the sources one: they went wrong together and were corrected separately, `extraPaths` lagging by several commits because nothing was watching it. An absent block passes -- CI deletes it before installing and re-runs these hooks against what is left. The skip-locally, fail-in-CI asymmetry and its reasoning are `test_fixture_provenance.py`'s: a skip reads in a summary line exactly like a pass.
The hook's failing cases were miniature pyproject.toml files written here. A synthetic defect is written by somebody who already knows what the rule checks, so it exercises the rule rather than the mistake -- a gate proven against a made-up example is proven against the wrong thing. Both failing cases are now byte copies of this repository's own pyproject.toml, taken out of its history and held to it: pyproject_both_blocks_redirected.toml 3cbf02a, both blocks on the scratch worktree pyproject_type_checker_path_left_behind.toml 82a512f^, sources corrected and extraPaths not The second is the interval between the two corrections, several commits during which the repository looked fixed. It is the case a hook covering only `[tool.uv.sources]` calls clean, and it asserts that the corrected half is *not* reported -- so what fails there is the half that was genuinely still broken. Each copy has a `.source` recording its commit, and a guard holding the bytes to it, or the obvious way to make a failing case pass is to edit the fixture. That guard skips when the object is unreachable, which this directory's other provenance guard refuses to do. The difference is that a commit is content-addressed: `3cbf02a:pyproject.toml` cannot become different bytes, so the comparison can only ever report a copy that was wrong when it was vendored, and it reports that wherever the history exists. A shallow clone has no object to compare against, and asserting anyway would fail on the clone rather than on the fixture. Every case drives `scripts/check-library-path.py` as a subprocess, the way prek invokes it -- never the module's functions, never the rule restated in the test. `test_the_hook_prek_runs_is_the_one_tested` closes what that leaves open by reading the entry back out of `prek.toml`: rename the script or the hook and every other case here goes on passing against something prek no longer runs, and an unregistered hook produces no output either. It reads the file as text because `prek.toml` spreads each hook's inline table over several lines, which is TOML 1.1 and `tomllib` rejects. The two vendored copies join the adapter captures in being exempt from the rewriting hooks, for the reason already recorded there: a formatter and a byte-identical fixture cannot both be unconditional, and it is the formatter that yields.
Grace period and dip compensation share a stored record but not a decision, and nothing pinned the boundary. While the panel is offline `_update_native_value` hands off to the grace period and returns before any dip processing, so an outage cannot book, confirm or retract anything. That division is deliberate: grace period answers what a sensor should read while the panel is unreachable, dip compensation answers what the counter did, and an outage is evidence about the transport rather than about the counter. The alternative matters — an outage landing between a dip and the reading that would settle it could otherwise confirm a bad offset by default, which is the same shape as forgetting the pending record across a restart. Covers the three parts: the pending record and offset survive the outage untouched, grace holds the compensated value HA was already showing, and the dip settles normally once the panel returns.
…ffset Two corrections to the README, both consequences of the retraction rule. The feature section said a persistent notification lists affected sensors "when a dip is detected". That is no longer true and teaches the wrong model: a dip is compensated on sight but not believed, so the notification fires on corroboration a reading later, and a disproved dip produces none at all. Someone watching a momentary dip pass with no notification would reasonably conclude the feature had failed, when that silence is it working. The troubleshooting row offered dip compensation as the prevention for energy dashboard spikes, which after #259 is half the story -- compensation could manufacture them. It now also says how to check: an `energy_offset` far larger than the sensor's own reading is compensation for a dip that never happened, and toggling the option off discards it. It says plainly what that cannot do. Offsets booked before 2.1.0 were never corroborated, so the integration cannot tell a good one from a bad one retrospectively -- which is why there is no repair flow offering to. And clearing an offset fixes the future only; spikes already written into long-term statistics still need Adjust sum. Table rows reflow because the column padding is recomputed; the text change is confined to the one cell. Both edits are `scripts/fix-markdown.sh` output.
…ps creating `disabled` had to be a mode that stops *creating* controls rather than one that removes them, because entity_id is permanent here. Nothing asserted that. The existing test checks only that no entities are added, which both spellings satisfy -- a mode that removed every registry entry on the way past would have passed it unchanged. Its docstring claimed the other half anyway: "No control entities, and no registry entries removed either." A claim made in a docstring and checked nowhere is worse than an absent test, because it reads as coverage to anyone auditing by name -- which is how this gap was found. That sentence now says what the test actually proves and points at the one below. The new test registers an entry, customises its name and icon, runs setup under `disabled`, and asserts the entry survives with its entity_id, its unique_id mapping and its customisations intact. Customised rather than bare because the cost of getting this wrong is not the identifier alone: a user's name, area and icon travel with the registry entry and do not come back with a reissued id. Verified by injecting the defect -- removing the switch entries on the disabled path -- and confirming this fails, then restoring. A guard that has never been seen to fire is not known to be a guard.
The first beta whose pins resolve. b14 named span-panel-api 3.1.0 and both adapters at 1.1.0 while none of the three existed on PyPI, so the pins were honest about what the code needed and unsatisfiable by anyone but a developer with local path overrides -- deliberately, since the alternative was pins that resolved to a library without the changes. All three published today, so CI can now install what the manifest asks for. What the beta carries beyond b14: - The panel credential no longer travels or rests in clear. The passphrase is not persisted, and the CA is fetched and confirmed before registration rather than after, so the exchange that carries the passphrase and returns both credentials is the first thing protected rather than the last. - The panel's CA is pinned and will not silently re-anchor. A change raises a repair naming both fingerprints and re-pins only on an explicit confirmation. - Control commands are gated on who is asking, and every one is recorded -- refusals included, which are the entries worth having. - No control silently swallows a command that did not happen. A refused command raises where the user made it; so does one the transport never handed over. - An absent meter reading is no longer read as zero, and a compensation offset the next reading disproves is retracted rather than kept. The vendored schema-1 capture is verified against the released schema-1-v1.1.0 rather than against unreleased bytes, which is the first time that check has had a release to verify against.
Adds the energy dip fix (#259) and cuts every entry under Changed, Fixed and Security to a single sentence, without leaning on a dash to bolt a second clause onto the first. Also drops the paragraph on SPAN's upgrade timing from the release preamble, which restated what the section above it already says. What came out was mechanism and caveat: how the dip was mistaken for a counter reset, what happens to offsets stored by earlier releases, why a changed CA cannot be re-pinned automatically, what a refused control used to do instead. All of it is in the README's Security section and troubleshooting table, which is where someone chasing one of these looks. What was kept is what an upgrader cannot find out any other way: which entities are affected, that ids and history survive, and where a template reading a moved attribute should now point.
Setup showed a step headed "Trust this panel's certificate": a 64-character fingerprint, four paragraphs on what pinning does and does not protect against, and one button. It asked the user to make a decision they cannot make. At first contact there is nothing to compare the fingerprint against. SPAN publishes it nowhere, so the screen said "comparing this fingerprint against another source is what closes that case" and then offered no source. It also told them a device standing between Home Assistant and the panel "would still see them" -- accurate, unactionable, and delivered at the moment they are least equipped to weigh it. The only available answer was Submit, which makes the question decoration. The protection was never in the asking. It is in pinning *before* registration, so the exchange that carries the passphrase and returns both credentials runs over a verified connection. That is unchanged: the CA is fetched, the panel's own certificate is checked against it, and it is pinned, before authentication is offered. One less step, and the same guarantee. The fingerprint moves to where it can be acted on rather than acknowledged: diagnostics report it under `panel_ca`, setup logs it at INFO so the value is recoverable from the install that pinned it, and the Repair raised if it ever changes carries both the pinned and the observed value. That last one is the moment the question becomes answerable, because there is a prior value to compare against and something has actually happened. Kept: the error step. A CA that cannot be fetched, or a certificate the panel serves that its own authority did not sign, still stops setup and offers a retry -- that failure has a remedy, so it is worth a screen. `test_the_ca_is_pinned_before_the_passphrase_is_ever_sent` now asserts the property rather than the screen that used to carry it: when the auth menu is offered, the panel has been asked nothing yet, and when registration does run it carries a transport with an SSL context on port 443.
The HTTPS-port question exists for an install reaching the panel through something that moved the TLS port, and it is the right question when nobody else can answer it. A panel discovered from the simulator add-on is not that install: the add-on allocates a TLS port per panel and reallocates it across restarts, so it is the only party that knows, and it now publishes what it knows -- `https_port` in the Supervisor record, `httpsPort` in the mDNS TXT. Read both, and skip the prompt when either spoke. Asking after being told is asking a user to confirm a fact they have no way to check, with the wrong answer costing them a `ca_leaf_mismatch` and no way past it. Nothing moves for hardware. Real firmware publishes no TLS port, so the flag stays false and the gate is what it was: no prompt on port 80, a prompt on a moved one. A panel behind a reverse proxy advertises `httpPort` and nothing else, and is still asked -- reading a port from discovery must not become a way for that install to silently check the CA against 443. An unreadable value is treated as unpublished for the same reason. The ports also join the discovery update on an already-configured entry, since an entry left pointing at last run's ports cannot reach its panel. The inline TXT parse became `_discovered_port`, which both discovery steps now share; it keeps the old fallbacks exactly, including 80 for a value that will not parse.
Carries the discovery change: a panel that publishes its TLS port is taken at its word, so setting up a simulated panel no longer stops on a question only the add-on could answer. Needs PanelBench 2.2.0 on the other side to have a TLS port worth publishing.
The integration is moving off presetting entity ids onto supplying one base per circuit entity and letting Home Assistant compose the rest from the user's entity_id_parts. The base reads an existing entity's suffix wording back from its own id, so Recreate proposes it its own id; a new entity gets the noun-last wording the panel level already uses.
…r wrote Three platforms each rebuilt the expected string by hand against only the description's current label, so a relabel silently stopped the release for that platform. One helper takes every label the description has carried.
Core reads suggested_object_id into object_id_base and composes the rest from the user's entity_id_parts. Entities that set no base are unchanged.
The integration no longer presets a circuit sensor's entity_id. It hands Core one flag-correct base and Core assembles the id from the user's entity_id_parts, exactly as every non-circuit entity already does. An existing entity keeps the suffix wording its id already has, so Recreate proposes it its own id; a new one gets the noun-last wording the panel level uses. The energy labels return to that wording too, and a name 2.0.8 wrote under either label is handed back.
…d otherwise move Handing Home Assistant the job of composing a circuit entity id changes how two shapes are spelled, for reasons belonging to this integration rather than to the user: a sensor shown on a sub-device card, whose id names the panel where composition names the charger, and any circuit entity on an install with the device prefix off, which has_entity_name prefixes anyway. Recreate would have offered both moves, and R1 says it must only ever offer one the user caused. Those entities go on being preset, from current panel data so a renamed circuit still refreshes the name half; everything new composes. The registry name release now reads its legacy labels through a typed narrow rather than getattr, and the two object-id hooks are one.
Same shape as the circuit sensors: one base, Core composes, the name an older release wrote is released through the shared helper. The breaker switch and the priority select were the last two circuit platforms spelling their own entity ids. Both now hand Home Assistant an object-id base -- the naming-flag half plus a suffix each names outright, "breaker" and "circuit_priority", since `get_user_friendly_suffix` has no entry for the one and maps the other to the unique_id's spelling. With the id no longer composed from the display name, the name is free to be the panel's in both naming modes rather than the mode's on a first install and the panel's afterwards. It travels as `original_name`, which ranks below the base, so what is shown cannot decide what "Recreate entity IDs" proposes -- and an unnamed circuit is named for its tabs instead of being left to HA, which gave every one of them the same id stem. R1's exception is now asked once. The two shapes composition spells differently -- a sensor on a sub-device card, and any circuit entity on an install with the device prefix off -- were a sensor-only decision; three platforms facing the same question with three copies of the answer is how they drift apart on which entities a release moves. The decision moves to `naming.legacy_preset_for_existing` and the sensors ask it too, keeping only the fact a sensor class alone knows: whether this integration ever preset that entity's id at all.
Four platforms now take the same route into the entity registry, and the helper that drove them had four copies of it: each of the switch, select and legacy-energy installs carried its own `load` differing from the sensor's only in the domain string and the constructor call, plus a `type: ignore[override]` to excuse a return type the base had fixed to one platform's entity. `_Install` becomes generic over the entity it installs and owns the one `load`. A subclass declares a `_domain` and a `_build`, so a change to the path under test -- the real `EntityPlatform` add, the `async_reset` teardown -- reaches all four at once instead of three of them drifting. The legacy install keeps its registry seeding as its own hook, since several of its cases exist to test the unseeded path. No assertion changed. The power-sensor install is now `_SensorInstall`, the base name having gone to the generic; its call sites moved with it.
construct_single_circuit_entity_id and preserve_legacy_entity_id_suffix are replaced by naming.circuit_object_id_base; the multi-circuit and unmapped builders had no callers. Three things fell out with them. _generate_friendly_name was dead across the integration -- fourteen implementations existed only to satisfy an abstract declaration nothing called, since _attr_name has always come from _generate_panel_name. The `name` constructor parameter on the breaker switch and the priority select had already been documented as deliberately unused, so it goes rather than stay as a trap. And sensor_circuit's _LOGGER had no call sites left. LEGACY_ENTITY_ID_SUFFIXES is superseded by naming.ENTITY_ID_SUFFIX_FORMS, which records every wording an id has shipped with rather than only the pre-preset ones; the paragraph of the three suffix-mapping docstrings that pointed at it now points at naming.py. CIRCUIT_SUFFIX_MAPPING itself is untouched -- it builds unique ids and is closed.
…el carries
Home Assistant slugifies "{device} {base}" as one string; the deleted preset
builder slugified the circuit name alone and joined the parts with `_`. Pins
that the two agree for the names a panel can actually carry -- punctuation,
runs of whitespace, accents -- so no new install diverges from today's shape.
One case disagreed, and the old builder won. Its suffix-omission test was
`circuit_part.endswith(f"_{suffix}")`, with a leading underscore, so a circuit
named exactly "Power" kept both halves and got `..._power_power`.
`circuit_object_id_base` tested without the underscore and would have offered
every such circuit a rename to `..._power` -- a move the user did not cause,
which R1 forbids. The rule now carries the underscore, and "Solar Power" still
omits the word as it always did.
Also drops an unused logger from sensor_panel.py and corrects a test docstring
that claimed every panel sensor takes its displayed name from
`_generate_panel_name`; the constructor does not ask it for a description that
declares a translation_key, whose label comes from translations.
…h the reload it requests Pins phase 2 name sync end to end for sensor, switch and select in both naming modes, through a real EntityPlatform: the rename asks for a reload, the reload rebuilds original_name, the entity id does not move, and a name the user set is left alone. The README promises this in both modes and the composition change moved where the name is written, so the promise needed a case that can see the registry. The per-platform unit tests drive the same code against a MagicMock registry, which cannot tell `original_name` from `name` -- and writing `name` is exactly the shortcut that made Recreate propose a friendly-name id for a circuit-numbered entity. Each case settles the install with a reload and an unchanged push before renaming, so the reload it then asks for is the one the rename caused and not the one every freshly-added entity asks for on its first update.
The circuit sensors, the breaker switch and the priority select no longer preset an entity_id; each supplies a base and Home Assistant composes the rest from entity_id_parts. Record what an upgrading user sees: nothing moves until Recreate is pressed, what Recreate then offers, the noun-last energy suffixes for new sensors, the two populations that keep a preset, the tab fallback for an unnamed circuit, and the device prefix that now appears in a circuit-numbers install's displayed name. Drop the 2.1.0 bullet announcing the energy-label rename: the labels are back to what 2.0.8 shipped, so only the betas ever saw the other wording. developer.md gains the mechanism -- naming.py as the only place circuit id wording is written, the suggested_object_id override, the legacy preset and why R1 requires it, and the release of the registry name field. The Recreate test whose docstring claims both modes display the new name now asserts it.
The Changed bullet claimed the integration never writes or clears the registry name, which the Fixed bullet two lines down contradicts: the release does clear the value 2.0.8 wrote. Say the narrower true thing. Also: the displayed-name example uses the default device name Span Panel; the EVSE feed-circuit wording is "sensors created from this release on", since a charger added later to an existing install composes too; and the entity-id settings are named by their key rather than by a menu path nobody has verified against the 2026.8 frontend. developer.md gains the fact most likely to be "simplified" away later -- Core files suggested_object_id as object_id_base, which the user's parts prefix, while a preset entity_id becomes a verbatim unprefixed suggestion that outranks it -- plus the third gate keeping the unmapped-tab sensors off the preset path, and the platform count corrected to three.
…ack anchored on the circuit Two ways "Recreate entity IDs" still offered a move nobody asked for (R1). The config flow names the panel device: the second panel on a system becomes "Span Panel 2" without anyone typing it. Composition spells the device half from that name, while every circuit id already on that panel says `span_panel_` -- the literal the deleted preset builder fell back to, since it was never told the panel's name. So the whole panel was offered `sensor.span_panel_2_...`. The preset exception now covers a generated device name that is not "Span Panel", read from the registry's `name`. A name in `name_by_user` is the user's and still composes, because Home Assistant composes from `name_by_user or name` and following that is following the user. The suffix half was read back from the end of the existing id and the omission rule decided from the identifier alone, so the two could disagree with the id they were meant to reproduce. A circuit named "Current" had `..._current_power` read as the `current_power` form and was offered `..._current_current_power`; a circuit named "Solar Power" whose id kept both halves was offered `..._solar_power`. Both questions are one question, so where the id still names the circuit they are answered together, anchored on the identifier: what follows the name is the form, and a name the id stops at means the form was omitted. A circuit renamed on the panel is unchanged -- its id no longer names it, so the suffix is read from the end as before and the name half follows the panel, which is issue #252.
…sent Reauth builds its REST transport from the entry's stored data, and an entry with no usable pin got a plaintext one. Registration is the exchange that carries the panel passphrase and returns the eBus broker password, so on those entries the credentials crossed the network in the clear -- the exact thing pinning before registration exists to remove, and the opposite of what the release notes tell upgrading users. Three populations reach reauth unpinned: entries still recorded as api_version "v1", which fail setup before the deferred fetch can run and which the v7 migration does not flag; v2 entries missing their broker credentials, which fail setup the same way; and entries whose deferred fetch failed. A fourth, an entry whose stored PEM no longer loads, falls back to plaintext in panel_rest_transport and is now re-pinned as well. Reauth of such an entry now routes through the CA step -- fetch, leaf check, adopt -- and only then shows the reauth menu, so both authentication methods register over the anchor. A leaf that does not chain shows ca_leaf_mismatch with no way past, as on install. _update_v2_entry keeps the acquired CA and clears the pending flag, so the entry stays pinned instead of falling back to a plaintext CA fetch on every connect. An entry that already carried a usable pin is untouched and skips the step. The entry's stored HTTPS port is adopted on reauth so a panel behind a proxy is not asked again for a port it already recorded, and is persisted alongside the CA when the user does have to name it.
… actually names A panel's certificate names the addresses it already knows itself by -- its IP and its .local name. An FQDN joins that list only when register_fqdn tells the panel to regenerate the leaf, and that runs after authentication. Every check before it was made against the FQDN with hostname verification on, so it failed on the one thing that could not yet be true. Two flows were unusable as a result. A fresh install named by FQDN died at the CA step with ca_leaf_mismatch, which is deliberately a dead end with no way past, so the install could not complete at all. Reconfiguring a pinned entry to an FQDN died one step earlier: detect_api_version dialled the new name under the stored anchor and reported cannot_connect. Neither was caught because every FQDN test in the suite ran under an autouse fixture that stubs async_leaf_chains_to_ca and build_panel_ssl_context, so hostname verification was never exercised. The pre-registration bootstrap now dials the panel's resolved address: the CA fetch, the leaf check, registration, and the FQDN request itself. Hostname verification is not relaxed anywhere -- the address checked against changed, the check did not, and a leaf the published CA does not sign is refused exactly as before. A name that will not resolve falls back to dialling the name, which is what shipped. Only once the panel reports the regenerated certificate is the FQDN verified, on the broker port and again on the port the entry will store, before anything is persisted. check_fqdn_tls_ready took the anchor it verifies against by refetching the CA over plain HTTP every two seconds, which is a fresh trust decision on each poll against whatever answered -- and anything that can answer it can also serve a leaf signed by the CA it just handed over. It now takes the pinned PEM, carried on PanelRestTransport beside the context built from it. The plaintext fetch survives only for a reconfigure of an entry that never acquired an anchor. Folded in, from the review of the reauth pinning that preceded this: validate_v2_passphrase and validate_v2_proximity now require a transport instead of defaulting to a plaintext one, so the calls that carry the passphrase and return the broker password cannot be made unpinned by omission; the proximity pre-check, which decides whether the door challenge is believed, runs over the pin rather than an unverified client; a reauth that replaces a stored PEM this system can no longer load logs the new fingerprint at WARNING; and a stored HTTPS port counts as known only when it could actually be read, rather than when a default was substituted for it.
R1 was clarified on 2026-08-26 (evening) in the circuit-numbers spec: The base must reproduce an existing entity's id *when Home Assistant composes it under the entity-id options that match how that install was built*. That is what "never offer a rename the user did not cause" means: the base carries the install's own suffix wording (`consumed_energy` or `energy_consumed`, read back from the id) and the circuit's own name, so two spellings of ours or a relabel never produce an offer. It does **not** mean bypassing Home Assistant's `entity_id_parts` with a hard-coded id: where composition yields a different device part (an EVSE feed sensor on the charger's card, a second panel named "Span Panel 2", an install with the legacy no-prefix option), that is Home Assistant's configuration at work and the offer is legitimate. The "legacy preset" exceptions added during implementation on 2026-08-26 are therefore withdrawn. Nothing moves an id without the user pressing Recreate. Withdrawn accordingly: - naming.py: LEGACY_PRESET_DEVICE_NAME, generated_panel_device_name, legacy_preset_entity_id, _names_another_device, legacy_preset_for_existing, and the now-unused dr / HomeAssistant / DOMAIN / Final imports. The module docstring states the clarified rule. - entity.py: _generated_panel_device_name. - sensor_base.py: _preset_composition_would_move, _id_was_preset_by_this_integration, the `self.entity_id = preset` branch and the USE_DEVICE_PREFIX import. The base is set whenever _object_id_parts returns parts and the entity is not a sub-device. - sensor_circuit.py: both _id_was_preset_by_this_integration class attributes. - switch.py, select.py: the legacy_preset_for_existing call, the preset branch and the USE_DEVICE_PREFIX read. _span_object_id_base is always set. No `self.entity_id = ...` remains in the four circuit platforms; Home Assistant composes every id from the user's entity_id_parts. Kept, because they are base fidelity rather than a bypass: the per-install suffix read-back, the identifier-anchored form test, and the sub-device sensors that supply no base and compose from their label on the charger. Tests: the "offered its own id" cases for a sub-device sensor, a no-prefix install and a generated "Span Panel 2" panel now assert what Home Assistant composes under (DEVICE, ENTITY) parts -- sensor.span_panel_ev_charger_power, sensor.span_panel_refrigerator_power, sensor.span_panel_2_refrigerator_power and the two control equivalents -- and that the live entity_id is unchanged after two reloads (R5). The unit tests of the deleted symbols are gone; every suffix read-back, rename and circuit_object_id_base case is unchanged. README, developer.md and the 2.1.0 changelog describe the base and the read-back only, and say that these three populations may be offered the composed id, their entity-id settings deciding, with nothing moving until they accept.
The EVSE naming section still described the withdrawn exemption -- "sensors that already exist keep the IDs they have, which name the panel". Nothing keeps an id any more: a feed sensor's entity id names the charger, and an existing one is offered that id when the user presses Recreate. developer.md now states plainly that the suffix read-back does not reach sub-device sensors. It lives in circuit_object_id_base, which a sub-device sensor never calls -- with no base, Core composes the entity half from the description label. An existing feed energy sensor spelled `..._energy_consumed` is therefore offered `sensor.<charger>_consumed_energy`, both halves changing, and that is by design: reading a base back for these is the bypass the clarified R1 forbids. README also carried the R5 sentence twice over two paragraphs; it now appears once, in the paragraph that describes the offer.
fix: Net Energy under dip compensation and withdrawn circuit controls (2.1.2b6)
…check offsets first
fix: apply dip compensation changes made during start-up, keep its default off (2.1.2b7)
docs(changelog): consolidate 2.1.2 into user-facing grouped entries
chore(release): 2.1.2b8
docs(changelog): say to install 2.1.2 before firmware r202639
chore(release): 2.1.2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.