Skip to content

fix(catalog): stop badging five drivers Control when they refuse every command - #64

Open
HuggeK wants to merge 1 commit into
srcfl:mainfrom
HuggeK:fix/myuplink-read-only-catalog
Open

fix(catalog): stop badging five drivers Control when they refuse every command#64
HuggeK wants to merge 1 commit into
srcfl:mainfrom
HuggeK:fix/myuplink-read-only-catalog

Conversation

@HuggeK

@HuggeK HuggeK commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

No — myuplink cannot control anything, and the catalog said it could.

Its driver_command refuses every command it is handed:

function driver_command(_action, _power_w, _cmd)
    -- Read-only: no actuation in Step 1.
    return false
end

Its header opens with READ-ONLY telemetry, its DRIVER.description ends with
Observe-only — no control, and its only http_post is the OAuth token
refresh. Yet manifests/myuplink.yaml said control: true, so the page badged
it Control — the first thing an owner reads when deciding whether a driver
can run their heat pump.

Checking the rest of the catalog found four more in exactly the same state:

driver what its driver_command says
myuplink -- Read-only: no actuation in Step 1.
nibe_local -- Read-only: no actuation. The pump stays in aidMode=off.
pixii_pv -- Read-only driver, nothing to control.
solis_string bare return false
tibber -- Tibber Pulse is a read-only meter; no control surface.

All five are now control: false. Each was confirmed by reading its source,
not by pattern-matching — an earlier heuristic pass wrongly flagged sungrow,
solis and solaredge_*, which dispatch to helpers and genuinely do write.

Why no driver version had to move

control is not among the fields the channel copies into the signed
artifact, so unlike ders (#53) this costs no version. Verified rather than
assumed: I rebuilt the channel from 4b5b474 and compared all 80 artifact
digests against this tree — none moved.

The deeper issue this uncovered, which is NOT fixed here

The signed artifact does not use the manifest's control field at all:

controls = has_driver_command and not declares_read_only

It infers control from the mere presence of a driver_command entrypoint, so
a stub that always refuses reads to it as a control path. All five are still
published with control_enabled: true, read_only: false, and a write
permission.

The repo's own sanctioned fix is read_only = true in the driver's DRIVER
table. I prototyped it and backed it out, because for myuplink it breaks the
driver: the read-only guard replaces host.http_post with a function that
raises, and myuplink POSTs to /oauth/token for the token it reads with.
Exempting it would need a write permission on a read-only driver, which
ftw_repository explicitly forbids:

if read_only and write_permissions.intersection(permissions):
    raise RepositoryError(f"{driver_id}: read-only driver has a write-capable permission")

That invariant is sound and deliberate. Splitting "may POST to authenticate"
from "may write to the device" is a real design decision, so it belongs in its
own change with a maintainer's call — not slipped into a labelling fix.

How the guard was tested, including a bug in the guard itself

tests/test_control_claims.py compares the claim against the code, which
nothing did before. Reverting any of the five to control: true fails it by
name, with both ways out stated — verified by actually reverting myuplink and
watching it go red.

It also asserts at least 20 drivers still read as controlling, because a guard
that quietly stops recognising anything passes exactly as loudly as one that
works. That is not hypothetical: the guard's own unit test caught it while it
was being written, when the call regex counted function driver_command( as a
call and so found every driver busy — which would have made both main
assertions vacuous.

🤖 Generated with Claude Code

…y command

myuplink, nibe_local, pixii_pv, solis_string and tibber declared
control: true. All five have a driver_command that refuses
unconditionally, and four say so in a comment on the line above the
return false -- "Read-only: no actuation", "Tibber Pulse is a read-only
meter; no control surface".

The Control badge is the first thing an owner reads when deciding
whether a driver can run their heat pump or their battery, and for these
five it promised something the code refuses. They are now control:
false, which is what their sources have said all along.

No Lua changed and no artifact bytes moved: control is not among the
fields the channel copies into the signed artifact, so no version had to
move. Verified by rebuilding the channel from main and comparing all 80
digests.

tests/test_control_claims.py compares the claim against the code, which
nothing did before. A driver_command that calls nothing cannot reach a
write however it is invoked. Reverting any of the five fails it by name.
It also asserts at least 20 drivers still read as controlling, because a
guard that stops recognising anything passes exactly as loudly as one
that works -- which its own unit test caught while it was being written,
when the call regex counted `function driver_command(` and so found
every driver busy.

Signed-off-by: HuggeK <48095810+HuggeK@users.noreply.github.com>
Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com>
@HuggeK

HuggeK commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

Update — the "deeper issue" section above says the artifact half was prototyped and backed out. It has since become two PRs, so here is the current picture:

PR what it does state
#64 (this one) corrects the catalog label for the five drivers green, independent — merge any time
srcfl/ftw#743 lets a read-only driver sign in before it reads green
#65 makes the signed artifact agree that myuplink is read-only red, blocked

Merge order: ftw#743 must land and ship before #65. A read-only myuplink reaching a host without the FTW change loses its token refresh and stops reading entirely.

This PR is not part of that ordering. It changes no Lua and moves no artifact bytes — verified by rebuilding the channel and comparing all 80 digests — so it can merge independently of both.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants