-
Notifications
You must be signed in to change notification settings - Fork 10
feat(drivers): move devices off a driver the release replaces #1433
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| --- | ||
| "ftw": patch | ||
| --- | ||
|
|
||
| A device that runs the retired ESPHome DSMR driver entry `esphome-dsmr` moves to | ||
| the release's `esphome_dsmr` at start, and its settings are saved; the two were | ||
| the same driver under two names. An operator's own driver file of the old name is | ||
| left alone. The bundled drivers come from srcfl/device-drivers 92adaf0. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| package driverrepo | ||
|
|
||
| import ( | ||
| "log/slog" | ||
| "os" | ||
| "path/filepath" | ||
| "strings" | ||
|
|
||
| "github.com/srcfl/ftw/go/internal/config" | ||
| "github.com/srcfl/ftw/go/internal/drivers" | ||
| ) | ||
|
|
||
| // Replacement is a device moved from a retired catalog driver to the | ||
| // release's driver that replaces it. | ||
| type Replacement struct { | ||
| Driver string `json:"driver"` | ||
| From string `json:"from"` | ||
| To string `json:"to"` | ||
| LogicalPath string `json:"logical_path"` | ||
| } | ||
|
|
||
| // MigrateReplaced moves devices off a catalog driver that one of this | ||
| // release's drivers declares it replaces (DRIVER.replaces), such as | ||
| // esphome-dsmr folded into esphome_dsmr. A configured driver that runs the | ||
| // owner's selection of the retired driver now names the release's file, and | ||
| // the old selection ends. An operator's own file of the old name in userDir | ||
| // is left alone. cfg must hold portable driver paths ("drivers/x.lua"); the | ||
| // caller saves the configuration when anything moved. | ||
| func (m *Manager) MigrateReplaced(cfg *config.Config, userDir string) []Replacement { | ||
| if m.store == nil || m.bundledDir == "" || cfg == nil { | ||
| return nil | ||
| } | ||
| entries, err := drivers.LoadCatalog(m.bundledDir) | ||
| if err != nil { | ||
| return nil | ||
| } | ||
| replacing := make(map[string]drivers.CatalogEntry) | ||
| for _, entry := range entries { | ||
| for _, old := range entry.Replaces { | ||
| replacing[old] = entry | ||
| } | ||
| } | ||
| if len(replacing) == 0 { | ||
| return nil | ||
| } | ||
| active, err := m.store.ActiveDriverRepoInstalls() | ||
| if err != nil { | ||
| slog.Warn("driver repository: read active state", "err", err) | ||
| return nil | ||
| } | ||
| var moved []Replacement | ||
| for _, installed := range active { | ||
| replacement, ok := replacing[installed.DriverID] | ||
| if !ok { | ||
| continue | ||
| } | ||
| rel := strings.TrimPrefix(installed.LogicalPath, "drivers/") | ||
| if userDir != "" { | ||
| if _, err := os.Stat(filepath.Join(userDir, filepath.FromSlash(rel))); err == nil { | ||
| continue | ||
| } | ||
| } | ||
| target := "drivers/" + replacement.Filename | ||
| var names []string | ||
| for i := range cfg.Drivers { | ||
| if filepath.ToSlash(cfg.Drivers[i].Lua) == installed.LogicalPath { | ||
| cfg.Drivers[i].Lua = target | ||
| names = append(names, cfg.Drivers[i].Name) | ||
| } | ||
| } | ||
| if err := m.Deactivate(installed.LogicalPath); err != nil { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an operator deliberately rolled this old driver back to an earlier managed version, AGENTS.md reference: AGENTS.md:L175-L179 Useful? React with 馃憤聽/ 馃憥. |
||
| slog.Warn("driver repository: end replaced selection", "path", installed.LogicalPath, "err", err) | ||
| } | ||
| for _, name := range names { | ||
| slog.Info("driver repository: device moved to the release's replacing driver", | ||
| "driver", name, "from", installed.DriverID, "to", replacement.ID, "path", target) | ||
| moved = append(moved, Replacement{Driver: name, From: installed.DriverID, To: replacement.ID, LogicalPath: target}) | ||
| } | ||
| } | ||
| return moved | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| package driverrepo | ||
|
|
||
| import ( | ||
| "context" | ||
| "crypto/sha256" | ||
| "encoding/hex" | ||
| "os" | ||
| "path/filepath" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/srcfl/ftw/go/internal/components" | ||
| "github.com/srcfl/ftw/go/internal/config" | ||
| ) | ||
|
|
||
| // esphome-dsmr was folded into esphome_dsmr: a site that installed the old | ||
| // catalog entry moves to the release's driver instead of keeping a copy that | ||
| // no longer gets updates. | ||
| func TestDeviceOnARetiredDriverMovesToTheReleasesReplacement(t *testing.T) { | ||
| for _, tc := range []struct { | ||
| name string | ||
| ownFile bool | ||
| wantMoved bool | ||
| }{ | ||
| {"moves", false, true}, | ||
| {"an operator's own file stays", true, false}, | ||
| } { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| site := newSupersedeSite(t, "1.0.3", "") | ||
| old := []byte(strings.Replace(string(testDriver("1.0.3")), `id = "demo"`, `id = "demo-old"`, 1)) | ||
| site.channel.mu.Lock() | ||
| site.channel.driver = old | ||
| sum := sha256.Sum256(old) | ||
| site.channel.manifest.Drivers[0].ID = "demo-old" | ||
| site.channel.manifest.Drivers[0].Path = "drivers/demo-old.lua" | ||
| site.channel.manifest.Drivers[0].Filename = "demo-old.lua" | ||
| site.channel.manifest.Drivers[0].SHA256 = hex.EncodeToString(sum[:]) | ||
| site.channel.manifest.Drivers[0].URL = site.channelURL + "/demo.lua" | ||
| site.channel.manifest.Drivers[0].HostAPI = components.CompatibleRange{Min: 1, Max: 1} | ||
| site.channel.mu.Unlock() | ||
| replacement := strings.Replace(string(testDriver("1.0.6")), "capabilities = { \"meter\" },", | ||
| "capabilities = { \"meter\" },\n replaces = { \"demo-old\" },", 1) | ||
| if err := os.WriteFile(filepath.Join(site.bundled, "demo.lua"), []byte(replacement), 0o644); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| manager, _ := site.boot("v0.137.0-beta.2") | ||
| if err := manager.Refresh(context.Background(), "test"); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if _, err := manager.Install(context.Background(), "test", "demo-old", ""); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| userDir := t.TempDir() | ||
| if tc.ownFile { | ||
| if err := os.WriteFile(filepath.Join(userDir, "demo-old.lua"), old, 0o644); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| } | ||
| cfg := &config.Config{Drivers: []config.Driver{{Name: "p1", Lua: "drivers/demo-old.lua"}}} | ||
| moved := manager.MigrateReplaced(cfg, userDir) | ||
| if !tc.wantMoved { | ||
| if len(moved) != 0 || cfg.Drivers[0].Lua != "drivers/demo-old.lua" { | ||
| t.Fatalf("moved = %+v, lua = %s; the operator's own file must stay", moved, cfg.Drivers[0].Lua) | ||
| } | ||
| return | ||
| } | ||
| if len(moved) != 1 || moved[0].From != "demo-old" || moved[0].To != "demo" || cfg.Drivers[0].Lua != "drivers/demo.lua" { | ||
| t.Fatalf("moved = %+v, lua = %s; want p1 on the release's drivers/demo.lua", moved, cfg.Drivers[0].Lua) | ||
| } | ||
| if active, _ := site.store.ActiveDriverRepoInstalls(); len(active) != 0 { | ||
| t.Fatalf("the retired selection is still active: %+v", active) | ||
| } | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If
SaveStoredfails afterMigrateReplacedsuccessfully deactivated the old selection, startup continues using the in-memory replacement while durable settings still name the old path. On the next restart there is no active old install to trigger this migration again, so the configured driver resolves to a missing retired file and may leave the site meter offline; make the save failure fatal or restore the old activation/configuration before continuing.AGENTS.md reference: AGENTS.md:L20-L25
Useful? React with 馃憤聽/ 馃憥.