Fix Shelly unreachable: roll back engine state on failed actions

Without this, a failed Execute() left engine state diverged from hardware.
SyncHardwareState would then misread the mismatch as a manual override and
apply a 1-hour lockout — causing either a stuck-on or stuck-off loop.

Now Execute() returns per-action []error. The control loop calls
Engine.RollbackAction() for each failed action, keeping engine state in
sync with hardware so the next cycle simply retries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-08 20:44:08 +02:00
parent 2dd53d1543
commit f45d23df97
4 changed files with 57 additions and 22 deletions

14
main.go
View File

@@ -361,8 +361,10 @@ func runCycle(
return
}
if err := act.Execute(ctx, actions); err != nil {
logger.Error("execution failed", "error", err)
for i, err := range act.Execute(ctx, actions) {
if err != nil {
eng.RollbackAction(actions[i])
}
}
}
@@ -520,8 +522,8 @@ func wwResetHandler(act *actuator.Actuator, eng *engine.Engine, cfg *config.Conf
TargetTempC: cfg.Strategic.WWBaseC,
Reason: "manual WW reset via web UI",
}
if err := act.Execute(ctx, []engine.Action{action}); err != nil {
logger.Error("WW reset: actuator failed", "error", err)
if errs := act.Execute(ctx, []engine.Action{action}); errs[0] != nil {
logger.Error("WW reset: actuator failed", "error", errs[0])
} else {
logger.Info("WW boost reset", "base_c", cfg.Strategic.WWBaseC, "locked_until", midnight.Format("15:04"))
}
@@ -611,8 +613,8 @@ func overrideHandler(act *actuator.Actuator, eng *engine.Engine, logger *slog.Lo
TurnOn: turnOn,
Reason: fmt.Sprintf("manual override via web UI (%s)", duration),
}
if err := act.Execute(ctx, []engine.Action{action}); err != nil {
logger.Error("web UI override failed", "consumer", consumerKey, "state", stateVal, "error", err)
if errs := act.Execute(ctx, []engine.Action{action}); errs[0] != nil {
logger.Error("web UI override failed", "consumer", consumerKey, "state", stateVal, "error", errs[0])
http.Error(w, "switch failed — check logs", http.StatusInternalServerError)
return
}