Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
* @srcfl/core
/packages/ @srcfl/core
/spec/ @srcfl/core
/spec/control-runtime-v2.md @srcfl/core
/spec/schemas/sourceful.driver-command.v1.schema.json @srcfl/core
/spec/schemas/sourceful.driver-command-result.v1.schema.json @srcfl/core
/drivers/lua/ @srcfl/core
/.github/ @srcfl/core
12 changes: 3 additions & 9 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,16 @@ See CONTRIBUTING.md. -->
- [ ] Cached telemetry becomes stale instead of being re-emitted as fresh.
- [ ] No credentials, serial numbers, private addresses or site data are included.

## Package or promotion evidence
## Control evidence

Complete this part when a package version, target, control contract or release
candidate changes. Use `not applicable` for a driver-only read-only change.
Complete this part when a control path changes. Use `not applicable` for a
read-only change.

- Public source SHA:
- Package ID and new version:
- Target matrix:
- Material and candidate artifact SHA-256:
- HIL evidence or `required`:
- Control enabled: `false` unless all control gates have passed
- [ ] Control changes include default-mode, lease-expiry and HIL evidence.

## Checks

- [ ] Commits include `Signed-off-by`.
- [ ] `make test-driver ID=<id>`
- [ ] `make package-driver ID=<id> TARGET=<target>`
- [ ] `make check`
19 changes: 0 additions & 19 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,25 +85,6 @@ jobs:
- run: git diff --exit-code -- support-status.json SUPPORT_STATUS.md
- run: uv run --no-sync pytest -q drivers/tests tests

packages:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
fetch-depth: 1
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
with:
python-version: "3.12"
- run: uv sync --frozen --extra package --extra dev
- name: Build each unsigned package candidate
run: |
for source in packages/v1/*/package-source.json; do
id="$(basename "$(dirname "$source")")"
uv run --no-sync python tools/build_candidate.py \
--id "$id" \
--output-dir "/tmp/device-driver-candidates/$id"
done

# The sign-off certifies authored content, so this asks it of the commits that
# carry some, and skips merge commits.
#
Expand Down
48 changes: 21 additions & 27 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# Device driver repository guide

This public repository is the only editable source for shared Sourceful device
drivers and the main driver source for FTW. It publishes FTW's signed driver
channel from reviewed commits. Device Support may later consume a locked commit
for other products or support levels, but it does not own a second source tree.
This public repository is FTW's driver repository and the only editable source
for its device drivers. It publishes FTW's signed driver channel from reviewed
commits.

## FTW product direction

Expand All @@ -30,43 +29,42 @@ and control acceptance gates below.
- Keep signing keys out of source, logs and build output.
- Public pull-request builds stay unsigned. The release workflow signs the FTW
channel only after a reviewed change reaches `main`.
- A catalog or package build never grants activation or control authority.
- A catalog build never grants activation or control authority.
- New drivers start read-only.
- Control needs a safe default mode, bounded leases, structured results and HIL
acceptance for every target host.

## Where a driver change has to land

A driver can exist in three places. Fixing one and leaving the others is how a
A driver can exist in two places. Fixing one and leaving the other is how a
fixed bug comes back.

1. **`drivers/lua/<id>.lua`** — the catalog driver. This is what the signed
channel publishes and what FTW bundles. Every fix starts here.
2. **`packages/v1/<id>/targets/*.lua`** — a separate file, not generated from
the catalog driver. Only some drivers have one. A package target carries its
own version line and can drift from the catalog copy without any check
noticing. If the driver you are fixing has one, fix both.
3. **FTW's `drivers/`** — a recovery snapshot, generated from this repository
channel publishes and what FTW bundles. Every fix starts here, and it is
the only copy of a driver in this repository.
2. **FTW's `drivers/`** — a recovery snapshot, generated from this repository
at the commit pinned in FTW's `drivers/BUNDLED_SOURCE.json`. Never edit a
driver there; FTW's own CI rejects the drift. But note the reverse: merging
here does **not** reach that snapshot. Someone has to move the pin and run
FTW's `scripts/sync-bundled-drivers.sh`. Until then a gateway booting
offline still runs the old driver.

Pixii's flap on register 40288 is the worked example. It was fixed in #16,
survived in the package target, was reverted in the catalog driver by #27, and
reached customer hardware a second time. See the entries for **pixii** 2.1.1
and **solaredge_legacy** 0.3.1 in `CHANGELOG.md`.
survived in a separate package-target copy, was reverted in the catalog driver
by #27, and reached customer hardware a second time. See the entries for
**pixii** 2.1.1 and **solaredge_legacy** 0.3.1 in `CHANGELOG.md`. Those copies
have since been removed.

### Editing a driver that came from FTW
### One id and one version

The 37 drivers promoted in #27 are exempt from this suite's catalog
conventions for as long as they stay byte-identical to `baselines/ftw/drivers`
— `drivers/tests/conftest.py` decides that by content, not by a list. The
moment you edit one, every check in the suite starts applying to it, and
`make check` may fail on rules that driver never had to meet. That is working
as intended. Read the failure before assuming your change caused it: the
rule may be right and the driver wrong, or the check itself may be wrong.
A driver's id is its file name and catalog name, and its `DRIVER` table says
exactly that id and the manifest's version. FTW compares its bundled copy with
the signed channel by that id and version, so they must be the same text. The
channel build and `tests/test_driver_truth.py` refuse anything else. Change a
version with `make bump-driver`, which moves both.

`baselines/ftw` records what was promoted from FTW in #27. Every promoted
driver has since changed, so no check treats them differently.

## Source rules

Expand All @@ -82,17 +80,13 @@ rule may be right and the driver wrong, or the check itself may be wrong.
caught there rather than by a wrong value on a customer's site. A document
behind a login cannot be watched; reference it in a driver comment instead.
See `docs/WRITING-A-DRIVER.md`.
- Keep Lua compatible with every runtime declared in the package recipe.
- Package id, version, read-only state and target metadata must match the Lua
`DRIVER` block.

## Checks

Run the narrow driver command while editing, then the full check:

```bash
make test-driver ID=example
make package-driver ID=example TARGET=ftw-core
make check
```

Expand Down
124 changes: 124 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,125 @@
# Changelog

## ctek 0.3.3

The `DRIVER` table's id is now `ctek`, the catalog id, instead of `ctek-chargestorm`. FTW compares the id of its bundled copy with the signed channel's to decide which file runs; the old spelling kept them from matching. No behaviour change.

## ctek_hybrid 0.3.2

The `DRIVER` table's id is now `ctek_hybrid`, the catalog id, instead of `ctek-chargestorm-hybrid`. FTW compares the id of its bundled copy with the signed channel's to decide which file runs; the old spelling kept them from matching. No behaviour change.

## ctek_v2 0.3.3

The `DRIVER` table's id is now `ctek_v2`, the catalog id, instead of `ctek-chargestorm-v2`. FTW compares the id of its bundled copy with the signed channel's to decide which file runs; the old spelling kept them from matching. No behaviour change.

## easee_cloud 1.3.4

The `DRIVER` table's id is now `easee_cloud`, the catalog id, instead of `easee-cloud`. FTW compares the id of its bundled copy with the signed channel's to decide which file runs; the old spelling kept them from matching. No behaviour change.

## ferroamp_modbus 2.1.3

The `DRIVER` table's id is now `ferroamp_modbus`, the catalog id, instead of `ferroamp-modbus`. FTW compares the id of its bundled copy with the signed channel's to decide which file runs; the old spelling kept them from matching. No behaviour change.

## fronius_smart_meter 2.1.3

The `DRIVER` table's id is now `fronius_smart_meter`, the catalog id, instead of `fronius-smart-meter`. FTW compares the id of its bundled copy with the signed channel's to decide which file runs; the old spelling kept them from matching. No behaviour change.

## huawei 2.1.3

The `DRIVER` table's id is now `huawei`, the catalog id, instead of `huawei-sun2000`. FTW compares the id of its bundled copy with the signed channel's to decide which file runs; the old spelling kept them from matching. No behaviour change.

## nibe_local 1.2.1

The `DRIVER` table's id is now `nibe_local`, the catalog id, instead of `nibe-local`. FTW compares the id of its bundled copy with the signed channel's to decide which file runs; the old spelling kept them from matching. No behaviour change.

## pixii_pv 0.3.2

The `DRIVER` table's id is now `pixii_pv`, the catalog id, instead of `pixii-pv`. FTW compares the id of its bundled copy with the signed channel's to decide which file runs; the old spelling kept them from matching. No behaviour change.

## solaredge_legacy 0.3.3

The `DRIVER` table's id is now `solaredge_legacy`, the catalog id, instead of `solaredge-legacy`. FTW compares the id of its bundled copy with the signed channel's to decide which file runs; the old spelling kept them from matching. No behaviour change.

## solaredge_pv 1.2.2

The `DRIVER` table's id is now `solaredge_pv`, the catalog id, instead of `solaredge-pv`. FTW compares the id of its bundled copy with the signed channel's to decide which file runs; the old spelling kept them from matching. No behaviour change.

## solis_string 1.1.3

The `DRIVER` table's id is now `solis_string`, the catalog id, instead of `solis-string`. FTW compares the id of its bundled copy with the signed channel's to decide which file runs; the old spelling kept them from matching. No behaviour change.

## sungrow 1.5.9

The `DRIVER` table's id is now `sungrow`, the catalog id, instead of `sungrow-shx`. FTW compares the id of its bundled copy with the signed channel's to decide which file runs; the old spelling kept them from matching. No behaviour change.

## tesla_vehicle 0.2.3

The `DRIVER` table's id is now `tesla_vehicle`, the catalog id, instead of `tesla-vehicle`. FTW compares the id of its bundled copy with the signed channel's to decide which file runs; the old spelling kept them from matching. No behaviour change.

## tesla_wall_connector 0.1.1

The `DRIVER` table's id is now `tesla_wall_connector`, the catalog id, instead of `tesla-wall-connector`. FTW compares the id of its bundled copy with the signed channel's to decide which file runs; the old spelling kept them from matching. No behaviour change.

## zap 3.1.1

The `DRIVER` table's id is now `zap`, the catalog id, instead of `sourceful-zap`. FTW compares the id of its bundled copy with the signed channel's to decide which file runs; the old spelling kept them from matching. No behaviour change.

## zaptec_cloud 0.1.1

The `DRIVER` table's id is now `zaptec_cloud`, the catalog id, instead of `zaptec-cloud`. FTW compares the id of its bundled copy with the signed channel's to decide which file runs; the old spelling kept them from matching. No behaviour change.

## zuidwijk_p1 1.1.2

The `DRIVER` table's id is now `zuidwijk_p1`, the catalog id, instead of `zuidwijk-p1`. FTW compares the id of its bundled copy with the signed channel's to decide which file runs; the old spelling kept them from matching. No behaviour change.

## heishamon 0.5.1

The `DRIVER` table's version now matches the catalog. It said 0.4.0 while the manifest said 0.5.0, because the file was kept byte-identical to the copy promoted from FTW. No behaviour change.

## sdm630 2.0.2

The `DRIVER` table's version now matches the catalog; it said 1.1.1 while the manifest said 2.0.1. Drop the Blixt L1 `DRIVER_MANIFEST` table. FTW never read it, and this repository no longer builds packages for other products. No behaviour change.

## esphome_dsmr 1.0.4

The source is now the ESPHome DSMR driver itself. `drivers/lua/esphome_dsmr.lua` held a generated channel artifact committed as source, so the channel wrapped its header twice and the published file reported id `esphome-dsmr` 1.0.2. The tested hardware now matches the `esphome-dsmr` entry, and the manifest says `control: false`, since the driver is read-only. The separate catalog entry `esphome-dsmr` stays for installs that use it.

## ambibox_v2x 1.1.1

The `DRIVER` table's id is now `ambibox_v2x` instead of `ambibox-v2x`, and its version matches the catalog (it said 1.0.0 while the manifest said 1.1.0). FTW compares the id of its bundled copy with the signed channel's to decide which file runs; the old spelling kept them from matching. No behaviour change.

## ferroamp_dc2_v2x 2.1.1

The `DRIVER` table's id is now `ferroamp_dc2_v2x` instead of `ferroamp-dc2-v2x`, and its version matches the catalog (it said 1.1.0 while the manifest said 2.1.0). JSON payloads are decoded through `pcall`, like every other MQTT driver; a bad payload is still logged and ignored. Control logic is unchanged. Hardware-in-the-loop testing is still pending before stable.

## 50-125k-svk 0.2.4

Drop the Blixt L1 `DRIVER_MANIFEST` table. FTW never read it, and this repository no longer builds packages for other products. No behaviour change.

## 50-125k-svk-ac-slew 0.2.5

Drop the Blixt L1 `DRIVER_MANIFEST` table. FTW never read it, and this repository no longer builds packages for other products. No behaviour change.

## 50-125k-svk-slew 0.1.13

Drop the Blixt L1 `DRIVER_MANIFEST` table. FTW never read it, and this repository no longer builds packages for other products. No behaviour change.

## acuvim 0.4.3

Drop the Blixt L1 `DRIVER_MANIFEST` table. FTW never read it, and this repository no longer builds packages for other products. No behaviour change.

## deye-svk 0.2.2

Drop the Blixt L1 `DRIVER_MANIFEST` table. FTW never read it, and this repository no longer builds packages for other products. No behaviour change.

## foxess_h3_smart 0.9.6

Drop the Blixt L1 `DRIVER_MANIFEST` table. FTW never read it, and this repository no longer builds packages for other products. No behaviour change.

## konja-261-svk 0.3.2

Drop the Blixt L1 `DRIVER_MANIFEST` table. FTW never read it, and this repository no longer builds packages for other products. No behaviour change.

## easee_cloud 1.3.3

Omit `power_observed_at` when the power is unchanged since the last poll, so the host stamps the reading on arrival. Easee records TotalPower only when it changes, so a steady charge kept an old timestamp; FTW took it as a stale charger after three minutes and stopped the car every few minutes all night ([srcfl/ftw#1417](https://github.com/srcfl/ftw/pull/1417)). A new value still carries Easee's source time, and an offline charger (op_mode 0) still emits no sample.
Expand All @@ -17,6 +137,10 @@ Driver versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html

## [Unreleased]

### Removed

- **The Device Support package pilot and its format.** This is FTW's driver repository, and the pilot gave each packaged driver a third version line beside the catalog `DRIVER` version and the manifest version, under a package id that could not contain `_` where the catalog id does. Nothing published or consumed it: the release workflow never called the packager, CI built unsigned candidates only, and no release carries a package or index asset. Removed: `packages/v1/` (recipes for esphome-dsmr, foxess_h3_smart, pixii, sdm630 and sungrow, and the separate `pixii` and `sungrow` target files), `tools/driver_package.py`, `tools/build_candidate.py`, `tools/derive_sungrow_ftw_observe.py`, `make package-driver`, the `packages` CI job, the package-v1 specs and every `sourceful.driver-*` JSON schema, the driver inventory and Blixt reference specs, `package_id`, `candidate_package_version`, `stable_package_version` and `control_enabled` from `support-status.json`, and the four `support-status-overrides.json` entries (pixii, goodwe, sdm630, sungrow) that described the pilot's own package targets and candidate versions. The catalog page loses its always-empty "Stable package" column and those notes. `spec/control-runtime-v2.md` keeps only FTW Core's v2 command interface, and `spec/signing.md` only the FTW channel. Tests that ran a rule against both a catalog driver and its package target now run it against the catalog driver only. No driver source or version changed. A package can be rebuilt from the catalog driver the day one is needed; git history keeps the pilot at 489c937

### Added
- **nibe_local** 1.2.0 — the driver's first write path, and deliberately its only one: the pump's native **Solar PV surplus feed** ([srcfl/ftw#537](https://github.com/srcfl/ftw/issues/537)). The S-series was built to take a live "available solar power" number from NIBE's Modbus accessory (registers 2107/2109) and soak the surplus into heating and hot water using owner-tuned offsets; FTW now acts as that accessory. Control-by-hint: the pump's firmware decides what to do with the number, so a wrong value degrades to wasted comfort, never to unsafe operation. Off by default and triple-gated — host `capabilities.http.allow_write`, driver `write.solar_pv: true` with a mandatory `write.max_w` clamp ceiling, and the owner-side enable on the pump itself. The pump's timeout for a silently stopped feed is undocumented, so the driver does not lean on it: a dead-man's switch clears the feed when commands stop, `driver_default_mode` clears it on watchdog/stale-meter/stop, and a startup sweep clears a feed a crashed run left behind — and the sweep stays armed even when a config mistake (missing `max_w`) refuses new writes. All of that runs only while FTW runs, so the driver header documents the decommission step (turn 2107 off, or set the API read-only in menu 7.5.15) for the day it does not. The write API's most dangerous habit is covered too: the Local REST API rejects writes *inside an HTTP 200* ("error: read only value"), which the driver surfaces as an actionable error naming the installer menu (7.5.15) instead of reporting success. Requires `host.http_patch` from the FTW core; on older cores the driver states so and stays read-only. The `DRIVER` block declares the path as `write_capabilities = { "solar_pv" }`, which is what lets a host offer a switch for it: FTW's Settings screen renders the feed's controls only for a driver that states it has a write path, so the alternative was an owner hand-editing two keys in `config.yaml` to use the feature at all

Expand Down
11 changes: 3 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,12 @@ git checkout -b add-example-meter
make new-driver ID=example PROTOCOL=modbus KIND=meter
```

The generated package is read-only and targets FTW and Blixt through the shared
Lua 5.1 source profile. Edit the generated files rather than creating a second
manifest elsewhere.
The generated driver is read-only and comes with its manifest. Edit the
generated files rather than creating a second manifest elsewhere.

Every driver must:

- declare `DRIVER` metadata whose id and version match its package;
- declare `DRIVER` metadata whose id and version match its manifest;
- implement `driver_init`, `driver_poll`, `driver_cleanup` and a safe
`driver_default_mode` when control is later added;
- translate vendor signs at the driver boundary;
Expand Down Expand Up @@ -140,13 +139,9 @@ make history
```bash
make bootstrap
make test-driver ID=example
make package-driver ID=example TARGET=ftw-core
make check
```

The package command creates an unsigned candidate under `.artifacts/`. It does
not grant release or signing rights.

The FTW release build turns each source file into a signed, read-only Lua
artifact. The channel includes the full catalog and checks every generated
artifact against the FTW v1 lifecycle and host API. A merged source change
Expand Down
Loading
Loading