From 82226cb1b0742faaa0e85564f128ea0d3f1c5519 Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Fri, 25 Sep 2026 08:59:17 +0200 Subject: [PATCH 1/5] fix(drivers): one id per driver, from the bundled copy to the channel Moves the pin to srcfl/device-drivers#132, where every driver's DRIVER table states the catalog id and version. FTW's identity rule for the old spellings (IdentifiesSameDriver) goes: a bundled copy and a channel artifact are the same driver when their ids are equal. - Catalog tests look drivers up by the catalog ids. - The recovery-snapshot provenance test for esphome_dsmr is gone: the bundled file is now the driver's source, and sync-bundled-drivers.sh --check already binds every bundled file to the pinned commit. - setup.js recognises the Zap driver by its id `zap`. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MuerPFZFG88kgu8sWVHeq7 --- .changeset/one-id-drivers.md | 11 ++++++ drivers/BUNDLED_SOURCE.json | 2 +- go/internal/api/api_device_repository.go | 25 ++++---------- go/internal/api/api_device_repository_test.go | 21 +++++++----- go/internal/driverrepo/identity.go | 34 ------------------- go/internal/driverrepo/identity_test.go | 28 --------------- .../drivers/catalog_verification_test.go | 14 ++++---- go/internal/drivers/esphome_dsmr_test.go | 20 +---------- go/internal/drivers/ferroamp_modbus_test.go | 2 +- go/internal/drivers/solaredge_curtail_test.go | 4 +-- go/internal/drivers/solis_string_test.go | 2 +- web/setup.js | 2 +- 12 files changed, 44 insertions(+), 121 deletions(-) create mode 100644 .changeset/one-id-drivers.md delete mode 100644 go/internal/driverrepo/identity.go delete mode 100644 go/internal/driverrepo/identity_test.go diff --git a/.changeset/one-id-drivers.md b/.changeset/one-id-drivers.md new file mode 100644 index 000000000..4cd093fb1 --- /dev/null +++ b/.changeset/one-id-drivers.md @@ -0,0 +1,11 @@ +--- +"ftw": patch +--- + +Every bundled driver now declares the same id and version as the signed driver +channel, for example `sungrow` instead of `sungrow-shx` and `easee_cloud` instead +of `easee-cloud`. Choosing a version under Settings → Devices now works for these +drivers, "Use bundled" finds the release's copy, and installing a channel version +reaches the running device without a restart. The driver inventory sent to +Sourceful reports these ids. ESPHome DSMR is one driver again (`esphome_dsmr`); +the duplicate `esphome-dsmr` entry is gone, and an installed copy keeps running. diff --git a/drivers/BUNDLED_SOURCE.json b/drivers/BUNDLED_SOURCE.json index 0c2067dfb..08ba2f1f2 100644 --- a/drivers/BUNDLED_SOURCE.json +++ b/drivers/BUNDLED_SOURCE.json @@ -16,7 +16,7 @@ "from the signed channel. Run scripts/sync-bundled-drivers.sh to update." ], "repository": "srcfl/device-drivers", - "commit": "489c9373be1fb391d885b948bf736399b3e40f2e", + "commit": "b4f0e66d0e97f1a60198ef0e4dc5dd3ba1f6301c", "source_dir": "drivers/lua", "drivers": [ "ambibox_v2x", "ctek", "ctek_hybrid", "ctek_v2", "deye", "easee_cloud", diff --git a/go/internal/api/api_device_repository.go b/go/internal/api/api_device_repository.go index 3b3f7f330..c23d8b9cc 100644 --- a/go/internal/api/api_device_repository.go +++ b/go/internal/api/api_device_repository.go @@ -11,7 +11,6 @@ import ( "time" "github.com/srcfl/ftw/go/internal/config" - "github.com/srcfl/ftw/go/internal/driverrepo" "github.com/srcfl/ftw/go/internal/drivers" "github.com/srcfl/ftw/go/internal/state" ) @@ -410,31 +409,21 @@ func (r managedDriverRestartState) response(status string, artifact any) map[str } // sameDriverFile reports whether a driver file, declaring id, holds the -// driver of a signed artifact. An equal id is enough. The bundled sources -// spell ids their own way (easee-cloud for the channel's easee_cloud), so a -// file at the path the signed manifest names also counts when its id passes -// the publisher's identity rule. The rule alone is too loose: ctek-chargestorm- -// hybrid contains "ctek". -func sameDriverFile(path, id string, artifact state.DriverRepoInstall) bool { - if artifact.DriverID == "" || id == "" { - return false - } - if id == artifact.DriverID { - return true - } - return filepath.Base(path) == filepath.Base(artifact.LogicalPath) && - driverrepo.IdentifiesSameDriver(id, artifact.DriverID) +// driver of a signed artifact. A driver has one id everywhere: its source, +// the signed channel and the copy bundled with this release. +func sameDriverFile(id string, artifact state.DriverRepoInstall) bool { + return artifact.DriverID != "" && id == artifact.DriverID } // bundledDriverFor finds this release's copy of a managed driver: first the // file at the logical path the signed manifest names, then any bundled file -// declaring exactly the same id. +// declaring the same id. func (s *Server) bundledDriverFor(id, logicalPath string) (string, error) { rel := strings.TrimPrefix(filepath.ToSlash(filepath.Clean(logicalPath)), "drivers/") if logicalPath != "" && rel != "" && !strings.Contains(rel, "..") && !filepath.IsAbs(rel) { candidate := filepath.Join(s.deps.DriverDir, filepath.FromSlash(rel)) if entry, err := drivers.ParseCatalogFile(candidate); err == nil && - sameDriverFile(candidate, entry.ID, state.DriverRepoInstall{DriverID: id, LogicalPath: logicalPath}) { + sameDriverFile(entry.ID, state.DriverRepoInstall{DriverID: id}) { return candidate, nil } } @@ -507,7 +496,7 @@ func (s *Server) restartManagedDriversExpected(ctx context.Context, artifact sta // record still binds the path to the ID; all other files need metadata. if filepath.Clean(current.Lua) != filepath.Clean(activePath) { entry, err := drivers.ParseCatalogFile(current.Lua) - if err != nil || !sameDriverFile(current.Lua, entry.ID, artifact) { + if err != nil || !sameDriverFile(entry.ID, artifact) { continue } } diff --git a/go/internal/api/api_device_repository_test.go b/go/internal/api/api_device_repository_test.go index 88232aed3..c20a7f8cd 100644 --- a/go/internal/api/api_device_repository_test.go +++ b/go/internal/api/api_device_repository_test.go @@ -492,9 +492,8 @@ func TestDriverCatalogNamesTheDriversThatRunEachFile(t *testing.T) { } } -// publishAs publishes the fixture driver under the channel's own spelling, -// as device-drivers does: the bundled source declares id "esphome-dsmr", -// the signed channel calls the same file "esphome_dsmr". +// publishAs publishes the fixture driver on the channel under id, at +// drivers/. func (f *driverUpdateFixture) publishAs(id, filename, version string) { f.t.Helper() source := []byte(strings.Replace(string(updateDriverLua(version, "P1-123", `host.emit("meter", {w=103})`)), @@ -528,13 +527,17 @@ func (f *driverUpdateFixture) requestFor(id, action, body string, want int) map[ return response } -// On the home box "Use bundled" answered "no bundled file declares driver -// easee_cloud": the release's easee_cloud.lua declares "easee-cloud". The -// same spelling kept an install from reaching the running instance until -// Core restarted. The signed manifest names the file, and the publisher's -// identity rule ties the two spellings together. -func TestChannelAndBundledSpellingsOfOneDriverMeet(t *testing.T) { +// A driver has one id: the release's copy and the signed channel's file at +// the same path declare it alike. An install reaches the running instance, +// and "Use bundled" finds the release's copy again. On the home box the old +// spellings (easee-cloud, easee_cloud) broke both. +func TestReleaseCopyAndChannelMeetByTheirOneID(t *testing.T) { f := newDriverUpdateFixture(t, "running") + release := strings.Replace(string(updateDriverLua("1.0.2", "P1-123", `host.emit("meter", {w=102})`)), + `id = "esphome-dsmr"`, `id = "esphome_dsmr"`, 1) + if err := os.WriteFile(f.bundled, []byte(release), 0o644); err != nil { + t.Fatal(err) + } f.publishAs("esphome_dsmr", "esphome_dsmr.lua", "1.0.3") response := f.requestFor("esphome_dsmr", "install", `{"repository_id":"test"}`, 200) diff --git a/go/internal/driverrepo/identity.go b/go/internal/driverrepo/identity.go deleted file mode 100644 index 0021ace17..000000000 --- a/go/internal/driverrepo/identity.go +++ /dev/null @@ -1,34 +0,0 @@ -package driverrepo - -import "strings" - -// IdentifiesSameDriver reports whether a DRIVER table's declared id names the -// signed catalog driver catalogID. It is the publisher's own rule, the one the -// channel build enforces (srcfl/device-drivers tools/ftw_repository.py, -// _identifies_same_driver): source ids use hyphens and are often more -// specific, so the catalog id's words must appear in the declared id in -// order. "easee-cloud" names easee_cloud and "sungrow-shx" names sungrow; a -// growatt table in deye.lua does not. -// -// The rule is loose on purpose ("ctek-chargestorm-hybrid" also contains -// "ctek"), so callers pair it with the file the signed manifest names. -func IdentifiesSameDriver(declared, catalogID string) bool { - if declared == "" || catalogID == "" { - return false - } - words := func(value string) []string { - return strings.Split(strings.ReplaceAll(value, "-", "_"), "_") - } - remaining := words(declared) - next := 0 - for _, word := range words(catalogID) { - for next < len(remaining) && remaining[next] != word { - next++ - } - if next == len(remaining) { - return false - } - next++ - } - return true -} diff --git a/go/internal/driverrepo/identity_test.go b/go/internal/driverrepo/identity_test.go deleted file mode 100644 index 2b224134e..000000000 --- a/go/internal/driverrepo/identity_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package driverrepo - -import "testing" - -// The cases are the publisher's (device-drivers tools/ftw_repository.py). -func TestIdentifiesSameDriverFollowsThePublishersRule(t *testing.T) { - for _, tc := range []struct { - declared, catalog string - want bool - }{ - {"easee-cloud", "easee_cloud", true}, - {"esphome-dsmr", "esphome-dsmr", true}, - {"ctek-chargestorm", "ctek", true}, - {"ctek-chargestorm-hybrid", "ctek_hybrid", true}, - {"sungrow-shx", "sungrow", true}, - {"huawei-sun2000", "huawei", true}, - {"sourceful-zap", "zap", true}, - {"growatt", "deye", false}, - {"different-driver", "esphome-dsmr", false}, - {"cloud-easee", "easee_cloud", false}, - {"", "easee_cloud", false}, - {"easee-cloud", "", false}, - } { - if got := IdentifiesSameDriver(tc.declared, tc.catalog); got != tc.want { - t.Errorf("IdentifiesSameDriver(%q, %q) = %v, want %v", tc.declared, tc.catalog, got, tc.want) - } - } -} diff --git a/go/internal/drivers/catalog_verification_test.go b/go/internal/drivers/catalog_verification_test.go index 302148bf3..694bfd2ca 100644 --- a/go/internal/drivers/catalog_verification_test.go +++ b/go/internal/drivers/catalog_verification_test.go @@ -25,13 +25,13 @@ func TestCatalogVerificationStatus(t *testing.T) { status string }{ {"ferroamp", "production"}, - {"sungrow-shx", "production"}, - {"easee-cloud", "production"}, - {"ferroamp-modbus", "experimental"}, - {"sourceful-zap", "production"}, + {"sungrow", "production"}, + {"easee_cloud", "production"}, + {"ferroamp_modbus", "experimental"}, + {"zap", "production"}, {"deye", "experimental"}, {"solis", "experimental"}, - {"solis-string", "experimental"}, + {"solis_string", "experimental"}, {"tibber", "experimental"}, } for _, tc := range cases { @@ -53,7 +53,7 @@ func TestCatalogSourcefulZapIsReadOnly(t *testing.T) { t.Fatal(err) } for _, e := range entries { - if e.ID == "sourceful-zap" { + if e.ID == "zap" { if !e.ReadOnly { t.Fatal("Sourceful Zap must remain explicitly read-only until the local API has a semantic control endpoint") } @@ -68,7 +68,7 @@ func TestCatalogSourcefulZapIsReadOnly(t *testing.T) { return } } - t.Fatal("sourceful-zap missing from catalog") + t.Fatal("zap missing from catalog") } // Drivers at production status must also have a non-empty VerifiedBy diff --git a/go/internal/drivers/esphome_dsmr_test.go b/go/internal/drivers/esphome_dsmr_test.go index 6e9dedce4..f4ab1f12a 100644 --- a/go/internal/drivers/esphome_dsmr_test.go +++ b/go/internal/drivers/esphome_dsmr_test.go @@ -2,12 +2,10 @@ package drivers import ( "context" - "crypto/sha256" "encoding/json" "fmt" "net/http" "net/http/httptest" - "os" "strings" "testing" @@ -505,22 +503,6 @@ func TestESPHomeDSMR_BackoffResetsOnRecovery(t *testing.T) { } } -func TestESPHomeDSMRRecoverySnapshotProvenance(t *testing.T) { - const ( - sourceCommit = "2939543a2041a29566e6cf27a4eb5e4c69924de8" - assetURL = "https://github.com/srcfl/device-drivers/releases/download/drivers-beta/driver-esphome-dsmr-v1.0.2-c415e507f4371c85.lua" - wantSHA256 = "c415e507f4371c859fbf60827cc0704c5e24ad122dca1a9e5e1f190d67e852d1" - ) - raw, err := os.ReadFile("../../../drivers/esphome_dsmr.lua") - if err != nil { - t.Fatalf("read recovery snapshot: %v", err) - } - got := fmt.Sprintf("%x", sha256.Sum256(raw)) - if got != wantSHA256 { - t.Fatalf("recovery snapshot SHA-256 = %s, want %s from %s at %s", got, wantSHA256, assetURL, sourceCommit) - } -} - func TestESPHomeDSMR_NameDerivedPhaseObjectIDs(t *testing.T) { // Default ESPHome DSMR YAML derives object_ids from entity names // ("Current Phase 1" → current_phase_1) rather than the DSMR @@ -640,7 +622,7 @@ func TestESPHomeDSMRCatalogEntry(t *testing.T) { } var found *CatalogEntry for i, e := range entries { - if e.ID == "esphome-dsmr" { + if e.ID == "esphome_dsmr" { found = &entries[i] break } diff --git a/go/internal/drivers/ferroamp_modbus_test.go b/go/internal/drivers/ferroamp_modbus_test.go index e97d19b2a..1244b20dc 100644 --- a/go/internal/drivers/ferroamp_modbus_test.go +++ b/go/internal/drivers/ferroamp_modbus_test.go @@ -54,7 +54,7 @@ func TestFerroampModbusCatalogEntry(t *testing.T) { } var found *CatalogEntry for i, e := range entries { - if e.ID == "ferroamp-modbus" { + if e.ID == "ferroamp_modbus" { found = &entries[i] break } diff --git a/go/internal/drivers/solaredge_curtail_test.go b/go/internal/drivers/solaredge_curtail_test.go index f5b1ad696..de1a5eaa2 100644 --- a/go/internal/drivers/solaredge_curtail_test.go +++ b/go/internal/drivers/solaredge_curtail_test.go @@ -193,8 +193,8 @@ func TestSolarEdgeCatalogAdvertisesCurtail(t *testing.T) { } wantIDs := map[string]bool{ "solaredge": false, - "solaredge-pv": false, - "solaredge-legacy": false, + "solaredge_pv": false, + "solaredge_legacy": false, } for _, e := range entries { if _, ok := wantIDs[e.ID]; !ok { diff --git a/go/internal/drivers/solis_string_test.go b/go/internal/drivers/solis_string_test.go index c7ba3f315..aed8b9ae1 100644 --- a/go/internal/drivers/solis_string_test.go +++ b/go/internal/drivers/solis_string_test.go @@ -108,7 +108,7 @@ func TestSolisStringCatalogEntry(t *testing.T) { } var found *CatalogEntry for i, e := range entries { - if e.ID == "solis-string" { + if e.ID == "solis_string" { found = &entries[i] break } diff --git a/web/setup.js b/web/setup.js index 28f75388d..dec90c857 100644 --- a/web/setup.js +++ b/web/setup.js @@ -272,7 +272,7 @@ btn.disabled = false; var lines = []; - if (selectedCatalog.filename === 'zap.lua' || selectedCatalog.id === 'sourceful-zap') { + if (selectedCatalog.filename === 'zap.lua' || selectedCatalog.id === 'zap') { lines.push('This driver is the P1/HAN site meter by default. Add inverters, batteries and chargers as their own devices in FTW when you can. If Zap is the only reader, turn on PV or battery ingest later under Settings → Devices.'); } if (selectedCatalog.description) lines.push(selectedCatalog.description); From 95d6f8234b9adf99cbb1c3ca8046bff79a095713 Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Fri, 25 Sep 2026 09:01:36 +0200 Subject: [PATCH 2/5] docs(release): promote the driver channel as the last stable step drivers-stable only serves installs that read the channel (1.x-3.x) and the stable rows of the Versions list; 0.x runs the drivers its release bundles. Promoting it is one existing command after a stable Core release, not new automation. The proposal to promote it with Core goes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MuerPFZFG88kgu8sWVHeq7 --- AGENTS.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 30b63dc84..f064ff1d2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -175,8 +175,7 @@ and `ftw update` and `ftw rollback` move them with Core. A driver fix is a pin bump in the next beta. The signed driver channel serves installs that cannot take a new Core and lets an owner pick another signed version of one driver. Early access ends when a release catches up; an older version chosen on -purpose stays until the owner changes it. Proposed: `drivers-stable` carries the drivers of the -latest stable Core and is promoted with it. +purpose stays until the owner changes it. Native 0.x path: @@ -188,6 +187,9 @@ Native 0.x path: 4. Dispatch the same workflow for `v0.X.Y` stable, naming the tested beta. The workflow checks the source commit and release assets, and keeps GitHub `releases/latest` on the old 2.x line. +5. Promote the signed driver channel for installs that read `drivers-stable`: + `gh workflow run ftw-drivers-release.yml -R srcfl/device-drivers --ref main -f channel=stable`. + It promotes the driver commit already published as `drivers-beta`. Do not publish routine Docker releases. Existing 1.x, 2.x and 3.x installs remain on their current version until their owner uses the guided installer From 3f9d9284874fe1eba47ac2fcd966327b349ffe45 Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Fri, 25 Sep 2026 09:31:51 +0200 Subject: [PATCH 3/5] fix(drivers): pin the merged one-id device-drivers commit Moves the pin to bcf4683, srcfl/device-drivers#132 as merged on main, instead of the branch commit it was tested against before its rebase. The changeset no longer says esphome-dsmr is gone: #132 keeps that catalog entry for sites that installed it. The stable promotion step says what to do when main has moved past drivers-beta. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MuerPFZFG88kgu8sWVHeq7 --- .changeset/one-id-drivers.md | 4 ++-- AGENTS.md | 4 +++- drivers/BUNDLED_SOURCE.json | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.changeset/one-id-drivers.md b/.changeset/one-id-drivers.md index 4cd093fb1..334726c0b 100644 --- a/.changeset/one-id-drivers.md +++ b/.changeset/one-id-drivers.md @@ -7,5 +7,5 @@ channel, for example `sungrow` instead of `sungrow-shx` and `easee_cloud` instea of `easee-cloud`. Choosing a version under Settings → Devices now works for these drivers, "Use bundled" finds the release's copy, and installing a channel version reaches the running device without a restart. The driver inventory sent to -Sourceful reports these ids. ESPHome DSMR is one driver again (`esphome_dsmr`); -the duplicate `esphome-dsmr` entry is gone, and an installed copy keeps running. +Sourceful reports these ids. The bundled ESPHome DSMR driver is its real source +again; its published file used to report a wrong id. diff --git a/AGENTS.md b/AGENTS.md index f064ff1d2..aee07e961 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -189,7 +189,9 @@ Native 0.x path: `releases/latest` on the old 2.x line. 5. Promote the signed driver channel for installs that read `drivers-stable`: `gh workflow run ftw-drivers-release.yml -R srcfl/device-drivers --ref main -f channel=stable`. - It promotes the driver commit already published as `drivers-beta`. + It promotes the driver commit already published as `drivers-beta`. If it + refuses because `main` has moved past `drivers-beta`, publish beta first + (`-f channel=beta`), then promote. Do not publish routine Docker releases. Existing 1.x, 2.x and 3.x installs remain on their current version until their owner uses the guided installer diff --git a/drivers/BUNDLED_SOURCE.json b/drivers/BUNDLED_SOURCE.json index 08ba2f1f2..eb3be83ce 100644 --- a/drivers/BUNDLED_SOURCE.json +++ b/drivers/BUNDLED_SOURCE.json @@ -16,7 +16,7 @@ "from the signed channel. Run scripts/sync-bundled-drivers.sh to update." ], "repository": "srcfl/device-drivers", - "commit": "b4f0e66d0e97f1a60198ef0e4dc5dd3ba1f6301c", + "commit": "bcf4683586ca425791f0347641e3998b60938f22", "source_dir": "drivers/lua", "drivers": [ "ambibox_v2x", "ctek", "ctek_hybrid", "ctek_v2", "deye", "easee_cloud", From dc951e647be814310089d87fde772f15d714f640 Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Fri, 25 Sep 2026 09:40:57 +0200 Subject: [PATCH 4/5] docs(agents): a driver selection survives updates and rollbacks Matches #1428: a newer release driver runs over an early selection without discarding it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MuerPFZFG88kgu8sWVHeq7 --- AGENTS.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index aee07e961..06f850cbe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -174,8 +174,9 @@ drivers pinned in [`drivers/BUNDLED_SOURCE.json`](drivers/BUNDLED_SOURCE.json), and `ftw update` and `ftw rollback` move them with Core. A driver fix is a pin bump in the next beta. The signed driver channel serves installs that cannot take a new Core and lets an owner pick another signed version of one driver. -Early access ends when a release catches up; an older version chosen on -purpose stays until the owner changes it. +A newer driver installed early runs until a release brings a newer one; an +older version chosen on purpose stays until the owner changes it, and an +update or rollback never loses the owner's selection. Native 0.x path: From 4f06d7abe032367a3795d6632583c6e2c32b99cb Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Fri, 25 Sep 2026 09:54:29 +0200 Subject: [PATCH 5/5] fix(drivers): pin device-drivers 43710c8 (esphome_dsmr 1.0.5) srcfl/device-drivers#134 puts esphome_dsmr's host API fields on their own lines; FTW reads DRIVER fields line by line and its publication check refused the 1.0.4 file. The bundled set now publishes cleanly. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MuerPFZFG88kgu8sWVHeq7 --- drivers/BUNDLED_SOURCE.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/BUNDLED_SOURCE.json b/drivers/BUNDLED_SOURCE.json index eb3be83ce..7daa8bcba 100644 --- a/drivers/BUNDLED_SOURCE.json +++ b/drivers/BUNDLED_SOURCE.json @@ -16,7 +16,7 @@ "from the signed channel. Run scripts/sync-bundled-drivers.sh to update." ], "repository": "srcfl/device-drivers", - "commit": "bcf4683586ca425791f0347641e3998b60938f22", + "commit": "43710c841f4715bdbc6070bea38c89fc60fd830a", "source_dir": "drivers/lua", "drivers": [ "ambibox_v2x", "ctek", "ctek_hybrid", "ctek_v2", "deye", "easee_cloud",