fix(nibe_local): file the heat pump as a heat pump, not a meter - #53
Merged
Conversation
nibe_local's manifest declared ders: [meter]. It reads a NIBE S-series
heat pump and emits nothing but metrics -- hp_power_w, hp_used_power_w,
the temperatures and the lifetime energy counters, all via
host.emit_metric and never via host.emit("meter", ...), which the
driver header states is deliberate so a pump's draw cannot double-count
against the site's real grid meter. So the catalog advertised a DER the
driver does not provide.
The published catalog page badges and filters on this field, so the pump
sat under Meter and was missing from Heat pump. heishamon -- the other
metric-only heat pump -- has been ders: [heatpump] since it landed; this
matches it.
Manifest metadata only. The Lua source, its version and its sha256 are
untouched, so no host sees a new driver build. index.yaml and
devices.yaml are regenerated.
Signed-off-by: Hugo Karlsson <48095810+HuggeK@users.noreply.github.com>
Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com>
HuggeK
marked this pull request as ready for review
July 31, 2026 08:44
This was referenced Jul 31, 2026
HuggeK
added a commit
that referenced
this pull request
Jul 31, 2026
…h it
The driver release failed on main with
error: nibe_local: changed artifact needs a higher version than 1.1.0
#53 corrected the manifest from ders: [meter] to ders: [heatpump] and read
that as catalog-only metadata, so it left the version at 1.1.0. It is not
catalog-only. _load_channel copies ders into the artifact's capabilities and
the channel signs source and metadata as one file, so the correction moved
the published bytes under a version that has already shipped. The channel
refused, and it was right to: that immutability is what lets an operator name
the bytes a site is running.
So nibe_local takes a patch. Patch rather than minor because no register,
field or emitted metric changed -- only the DER the catalog states. The Lua
source is untouched and its sha256 with it, and bump_driver leaves the DRIVER
table alone because the file is byte-identical to its FTW baseline, so the
provenance guarantee #29 established is spent on nothing.
Verified against the currently published beta manifest: of 80 drivers exactly
one differs from what is published, and after the bump nothing is blocked.
CONTRIBUTING said to raise the version whenever the Lua source changes, which
is what #53 followed. It now also names the manifest fields that reach the
signed artifact -- ders, protocol, and a tested_devices manufacturer and
model_family -- and warns that this check runs on main after the merge rather
than in the pull request, because only the release can read the channel.
Signed-off-by: Hugo Karlsson <48095810+HuggeK@users.noreply.github.com>
Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com>
This was referenced Jul 31, 2026
HuggeK
added a commit
that referenced
this pull request
Jul 31, 2026
The signed channel refuses to republish changed artifact bytes under a version it has already published. That is the rule that makes a version name a set of bytes an operator can pin, and it was the one release check no pull request could run: asking it needs the published channel, so it only ever ran from the release job on main. #53 merged with five green checks and took the release down; the repair had to be a second pull request against a main that was already broken. Nothing about the question needs the signing key. The published manifest is a public release asset, its signature verifies with the public key already in the workflow, and the bytes a driver would publish as come from the tree. So check-versions builds the artifacts in memory, signs nothing, and answers what the release would answer -- on every pull request, forks included, since a fork needs no secret to read a public asset. _version_collisions is now the single statement of the rule. Both the release and the check read it, because a rule stated twice drifts, and a check that drifts from the release is worse than no check at all: test_check_versions_answers_what_the_release_would_answer holds them to the same verdict on the same tree. The check reports every colliding driver rather than the first, each with the bump_driver command that fixes it. The release still stops at one; it has nothing else to do, and someone fixing a branch wants the list. test_check_versions_catches_a_manifest_only_edit is #53 in the shape it arrived in -- a manifest field corrected, no Lua touched, the published bytes moved anyway. Verified against the live channel as well: the current main passes, and replaying that edit is refused before a merge. CONTRIBUTING no longer says this fails only after the merge, and carries the command to ask it locally. It also states the one sharp edge: the comparison is against the published channel, not against main, so a main that is itself unpublishable fails this on unrelated pull requests until it is fixed. Signed-off-by: Hugo Karlsson <48095810+HuggeK@users.noreply.github.com> Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com>
HuggeK
added a commit
that referenced
this pull request
Jul 31, 2026
myuplink declared ders: [], so the one MyUplink cloud heat-pump driver was absent from the Heat pump filter and from every other device type on the catalog page -- unreachable by the thing an owner actually searches for. It is now ders: [heatpump]. This revisits a deliberate choice. #27 widened the DER vocabulary and set myuplink to none, reading ders as the emit stream a driver produces rather than the device it covers. #53 settled it the other way for nibe_local, citing heishamon. All three report a pump through host.emit_metric and never call host.emit; myuplink and heishamon emit the same hp_* metric names. Two of the three already declared heatpump, so myuplink was the one left behind rather than the one holding a line. 1.1.0 -> 1.1.1 because ders is copied into the signed artifact's capabilities, so this changes the published bytes with no Lua moved -- the same reason nibe_local took 1.1.1. Unlike #53 the channel never got the chance to refuse it: the preflight check answers on the pull request, and putting the version back to 1.1.0 there reproduces the refusal. spec/manifest-v2.md documented four DER types where validate_manifest.py accepts seven. ev, heatpump and vehicle have been valid since #27 and are used by seven manifests, but the field table a driver author reads never listed them -- and the missing vocabulary is exactly what myuplink needed. The table is now complete and says what the field means for a metrics-only driver, which is the ambiguity that produced three different answers for three drivers doing the same thing. Signed-off-by: HuggeK <48095810+HuggeK@users.noreply.github.com> Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com>
HuggeK
added a commit
that referenced
this pull request
Jul 31, 2026
…o unlock it protocol names a transport and answers neither question an owner asks first. nibe_local and myuplink are both protocol: http and both read a NIBE heat pump, and they are opposites: one reads the pump over the LAN, the other reads NIBE's cloud. The catalog could not tell them apart. Two fields, orthogonal to protocol rather than folded into it. Splitting protocol into http_local/http_online was the obvious move and the wrong one: protocol feeds PROTOCOL_PERMISSIONS, so new values change what a package build grants, and it does not generalise -- MQTT can be a cloud broker too. connectivity: local | cloud where the driver talks while running setup: [...] what a human must obtain first, and from whom Backfilled across all 80 drivers from what the sources do. 77 local, 3 cloud: myuplink, easee_cloud and tibber are the only drivers that hardcode a vendor endpoint. Every other HTTP driver builds its base URL from config, and the Modbus, MQTT and serial drivers name a vendor only as homepage. setup is the second axis, and a driver can be local and still locked until somebody unlocks the interface: nibe_local is enabled in the pump's own installer menu -- no app, no account -- while sonnen and CTEK are enabled in the device's web UI, sma_pv needs an installer, and eight drivers need a bridge in between. 17 have a recorded gate; 63 do not, and absent means unrecorded. The page renders that as "Not recorded", never "Nothing to set up", and only setup: [none] claims a device needs nothing but an address. tests/test_connectivity.py stops connectivity being decorative. A cloud endpoint must be hardcoded -- there is nowhere else for it to come from -- while a LAN address comes from config, so the test reads that asymmetry in both directions and separates exactly the three from the other 77. Neither field reaches the signed artifact: _load_channel copies an explicit allowlist and these are not in it. So no driver version moves, unlike the ders correction in #53 that #55 had to version. Signed-off-by: Hugo Karlsson <48095810+HuggeK@users.noreply.github.com> Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com>
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.
nibe_localwas filed as a meter. It reads a NIBE S-series heat pump.The driver emits nothing but metrics —
hp_power_w,hp_used_power_w, the temperatures and the lifetime energy counters, all throughhost.emit_metricand never throughhost.emit("meter", …). Its header says that is deliberate: a heat pump is a load, and emitting it as a meter would double-count the pump's draw against the site's real grid meter. Soders: [meter]advertised a DER the driver does not provide.heishamon— the other metric-only heat pump in the catalog — has beenders: [heatpump]since it landed. This makesnibe_localmatch it.Why it is worth a PR of its own
The catalog page published by #51 badges and filters drivers by this field, so today the pump appears under Meter and is missing from Heat pump — where an owner looking for NIBE support would start. Rebuilt the page locally to confirm the fix lands:
Scope, and what deliberately did not change
Manifest metadata only. The Lua source, its version (
1.1.0) and itssha256are untouched, so no host sees a new driver build and the FTW baseline byte-identity exemption is unaffected.index.yamlanddevices.yamlare the regenerated outputs.myuplink— the cloud NIBE/Bosch/Daikin heat-pump driver — declaresders: [], andtools/ftw_repository.pydefends that in a comment: a driver that publishes only metrics should not have to claim telemetry it never emits. That reasoning sits awkwardly besideheishamon, which is also metric-only and is filed[heatpump]; the distinction seems to be thatheishamonhas a control path. Left alone here rather than folded into a one-line fix — happy to follow up either way, but the catalog should probably pick one rule.control: trueon this manifest predates the write path and is likewise untouched.Ordering
Two open branches also touch
manifests/nibe_local.yaml— #46 (Solar PV write path) and #48 (upstream_docs). Both are mine and both take a trivial rebase after this lands; this one is one line and independent, so it should go first.🤖 Generated with Claude Code