Fix compressor watts, add WW boost reset button

Compressor:
- metric changed to PromQL expression that returns actual watts:
  sensors_power_value(%) × power_value(kW) × 10
  e.g. 27% × 10 kW = 2700 W; idle = 0 W
- idle threshold raised 20→500 W (clean gap: 0W idle vs ≥2700W running)
- status page now displays real kW instead of percent

WW boost reset:
- Added 🌡️ Zurücksetzen button to WW consumer card (when configured)
- POST /ww/reset: restores WWBaseC via Viessmann API + blocks re-boost
  via engine override until midnight
- IsWW field added to consumerView for template control

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-06 16:00:42 +02:00
parent a537b02adb
commit 1b57293da9
3 changed files with 45 additions and 2 deletions

View File

@@ -10,7 +10,7 @@ prometheus:
battery_soc: "ess_stateOfCharge_value" # 0-100%
pv_production: "photovoltaic_production_current_value" # watts
battery_power: "ess_power_value" # watts
compressor_power: "heating_compressors_0_sensors_power_value" # % of rated power (0=idle, 27-92=running); _power_value is rated kW (constant 10)
compressor_power: "heating_compressors_0_sensors_power_value * heating_compressors_0_power_value * 10" # actual watts: sensors(%) × rated_kW(10) × 10
ambient_temp: "heating_sensors_temperature_outside_value" # °C
phase_l1_power: "pcc_ac_active_power_phaseOne" # per-phase grid power L1 (W)
phase_l2_power: "pcc_ac_active_power_phaseTwo" # per-phase grid power L2 (W)
@@ -78,7 +78,7 @@ strategic:
# Consumer behavior — idle detection thresholds
consumers:
compressor_idle_w: 20 # compressor idle threshold: sensors_power_value is % of rated power; 0%=idle, ≥27%=running → 20 cleanly separates them
compressor_idle_w: 500 # heat pump compressor below this = idle (W); running starts at ~2700W, so 500W cleanly separates idle (0W) from running
wallbox_min_charge_w: 50 # wallbox below this = car not charging (W)
idle_cycles: 3 # consecutive idle cycles before early release (~6 min at 2-min poll)

View File

@@ -133,6 +133,7 @@ type consumerView struct {
Reason string
Unconfigured bool
CanOverride bool // true for Shelly consumers (hardware read-back available)
IsWW bool // true for ConsumerWW — shows reset button instead of override
ManualOverride bool
OverrideUntil time.Time
LivePowerW float64 // from Shelly PM; 0 for non-PM devices
@@ -334,6 +335,7 @@ func (s *Store) Handler() http.HandlerFunc {
Since: rec.since,
Reason: rec.reason,
CanOverride: c != engine.ConsumerWW,
IsWW: c == engine.ConsumerWW,
ManualOverride: rec.manualOverride,
OverrideUntil: rec.overrideUntil,
LivePowerW: rec.livePowerW,
@@ -895,6 +897,11 @@ h1 { font-size: 1.4rem; font-weight: 700; color: #14532d; }
{{end}}
</form>
{{end}}
{{if and .IsWW (not .Unconfigured)}}
<form method="post" action="/ww/reset" style="flex-shrink:0">
<button type="submit" class="override-btn turn-off" title="WW-Solltemperatur auf Basiswert zurücksetzen, Boost für heute sperren">🌡️ Zurücksetzen</button>
</form>
{{end}}
</div>
{{end}}

36
main.go
View File

@@ -143,6 +143,7 @@ func main() {
mux.HandleFunc("/monitor", monitorHandler(monitorMode, logger))
mux.HandleFunc("/trip", tripSetHandler(tripMgr, cfg, logger))
mux.HandleFunc("/trip/cancel", tripCancelHandler(tripMgr, logger))
mux.HandleFunc("/ww/reset", wwResetHandler(act, eng, cfg, logger))
mux.HandleFunc("/", statusStore.Handler())
srv := &http.Server{
@@ -453,6 +454,41 @@ func tripCancelHandler(tm *trip.Manager, logger *slog.Logger) http.HandlerFunc {
}
}
// wwResetHandler resets the WW temperature to the configured base setpoint and
// blocks further boosts for the rest of the day via an engine override.
func wwResetHandler(act *actuator.Actuator, eng *engine.Engine, cfg *config.Config, logger *slog.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
// Block re-boost until midnight.
now := time.Now()
midnight := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, now.Location())
lockDur := time.Until(midnight)
eng.ApplyOverride(engine.ConsumerWW, false, lockDur)
// Immediately restore base WW temperature via Viessmann API.
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
defer cancel()
action := engine.Action{
Consumer: engine.ConsumerWW,
TurnOn: false,
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)
} else {
logger.Info("WW boost reset", "base_c", cfg.Strategic.WWBaseC, "locked_until", midnight.Format("15:04"))
}
http.Redirect(w, r, "/", http.StatusSeeOther)
}
}
// monitorHandler toggles monitor-only mode on POST and redirects to the status page.
func monitorHandler(mode *monitor.Mode, logger *slog.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {