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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
36
main.go
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user