Add configurable override duration and hard-stop thresholds
Override duration (web UI): - Dropdown on Ein button: 30min / 1h / 2h / 4h - Engine notified immediately via ApplyOverride() — no waiting for next SyncHardwareState cycle - Aus button always uses 1h lockout (keeps consumer off for 1h) Hard-stop thresholds that cancel active overrides: - SOC emergency brake: now also clears ManualOverride flag so EMS resumes full control after the safety shutdown - override_max_import_w (default 800W): if grid import exceeds this while an override is active, override is cancelled immediately (no hysteresis delay — protection is instant) Config: ems.override_max_import_w (0 = disabled) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
26
main.go
26
main.go
@@ -104,7 +104,7 @@ func main() {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("ok"))
|
||||
})
|
||||
mux.HandleFunc("/override", overrideHandler(act, logger))
|
||||
mux.HandleFunc("/override", overrideHandler(act, eng, logger))
|
||||
mux.HandleFunc("/", statusStore.Handler())
|
||||
|
||||
srv := &http.Server{
|
||||
@@ -269,10 +269,9 @@ func shouldRecover(stateFile string, timeout time.Duration, logger *slog.Logger)
|
||||
}
|
||||
|
||||
// overrideHandler handles manual on/off requests from the status page.
|
||||
// For Shelly consumers, it switches the hardware directly; the existing
|
||||
// SyncHardwareState mechanism detects the change next cycle and applies
|
||||
// the override lockout automatically.
|
||||
func overrideHandler(act *actuator.Actuator, logger *slog.Logger) http.HandlerFunc {
|
||||
// Switches the hardware immediately and informs the engine directly so
|
||||
// the override lockout is applied without waiting for the next cycle.
|
||||
func overrideHandler(act *actuator.Actuator, eng *engine.Engine, logger *slog.Logger) http.HandlerFunc {
|
||||
consumerByKey := map[string]engine.Consumer{
|
||||
"sg_ready": engine.ConsumerSGReady,
|
||||
"wallbox_a": engine.ConsumerWallboxA,
|
||||
@@ -287,6 +286,7 @@ func overrideHandler(act *actuator.Actuator, logger *slog.Logger) http.HandlerFu
|
||||
|
||||
consumerKey := r.FormValue("consumer")
|
||||
stateVal := r.FormValue("state")
|
||||
durationStr := r.FormValue("duration")
|
||||
|
||||
consumer, ok := consumerByKey[consumerKey]
|
||||
if !ok {
|
||||
@@ -294,6 +294,11 @@ func overrideHandler(act *actuator.Actuator, logger *slog.Logger) http.HandlerFu
|
||||
return
|
||||
}
|
||||
|
||||
duration, err := time.ParseDuration(durationStr)
|
||||
if err != nil || duration <= 0 {
|
||||
duration = time.Hour // safe default
|
||||
}
|
||||
|
||||
turnOn := stateVal == "on"
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||
@@ -302,7 +307,7 @@ func overrideHandler(act *actuator.Actuator, logger *slog.Logger) http.HandlerFu
|
||||
action := engine.Action{
|
||||
Consumer: consumer,
|
||||
TurnOn: turnOn,
|
||||
Reason: "manual override via web UI",
|
||||
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)
|
||||
@@ -310,7 +315,14 @@ func overrideHandler(act *actuator.Actuator, logger *slog.Logger) http.HandlerFu
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("web UI override executed", "consumer", consumerKey, "state", stateVal)
|
||||
// Inform engine immediately so override lockout is active before next cycle
|
||||
eng.ApplyOverride(consumer, turnOn, duration)
|
||||
|
||||
logger.Info("web UI override executed",
|
||||
"consumer", consumerKey,
|
||||
"state", stateVal,
|
||||
"duration", duration,
|
||||
)
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user