diff --git a/configs/ems-config.yaml b/configs/ems-config.yaml index f1f43fb..524e461 100644 --- a/configs/ems-config.yaml +++ b/configs/ems-config.yaml @@ -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) @@ -111,7 +111,8 @@ ems: recovery_timeout: "1h" # ignore Shelly state if EMS was down longer override_timeout: "1h" # default lockout when EMS detects external Shelly change override_max_import_w: 800 # cancel override immediately if importing more than this (0 = disabled) - monitor_only_file: "/etc/ems/monitor-only" # presence of this file = monitor-only mode active + monitor_only_file: "/etc/ems/monitor-only" # presence of this file = monitor-only mode active + ww_boost_disable_file: "/etc/ems/ww-boost-off" # presence of this file = WW boost disabled trip_goal_file: "/var/lib/ems/trip-goal.json" # persisted active trip goal session_log_file: "/var/lib/ems/sessions.jsonl" # JSONL log of completed charge sessions diff --git a/internal/config/config.go b/internal/config/config.go index ad04bcc..ff8f0f6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -167,6 +167,7 @@ type EMSConfig struct { OverrideTimeout string `yaml:"override_timeout"` OverrideMaxImportW float64 `yaml:"override_max_import_w"` // cancel override if grid import exceeds this (0 = disabled) MonitorOnlyFile string `yaml:"monitor_only_file"` // flag file path: presence = monitor-only mode active + WWBoostDisableFile string `yaml:"ww_boost_disable_file"` // flag file path: presence = WW boost disabled TripGoalFile string `yaml:"trip_goal_file"` // persisted active trip goal SessionLogFile string `yaml:"session_log_file"` // JSONL log of completed charge sessions } diff --git a/internal/status/status.go b/internal/status/status.go index 906392e..4ca0e7e 100644 --- a/internal/status/status.go +++ b/internal/status/status.go @@ -59,25 +59,27 @@ type Store struct { strategic config.StrategicConfig consumers map[engine.Consumer]*consumerRecord fcResult *forecast.Result - monitorMode *monitor.Mode - tripMgr *trip.Manager - carOptions []CarOption + monitorMode *monitor.Mode + wwBoostDisabled *monitor.Mode + tripMgr *trip.Manager + carOptions []CarOption } // NewStore creates a new status store. -func NewStore(dryRun bool, wwConfigured bool, strategic config.StrategicConfig, mode *monitor.Mode, tm *trip.Manager, cars []CarOption) *Store { +func NewStore(dryRun bool, wwConfigured bool, strategic config.StrategicConfig, mode *monitor.Mode, wwBoostDisabled *monitor.Mode, tm *trip.Manager, cars []CarOption) *Store { consumers := make(map[engine.Consumer]*consumerRecord, len(consumerOrder)) for _, c := range consumerOrder { consumers[c] = &consumerRecord{} } return &Store{ - dryRun: dryRun, - wwConfigured: wwConfigured, - strategic: strategic, - consumers: consumers, - monitorMode: mode, - tripMgr: tm, - carOptions: cars, + dryRun: dryRun, + wwConfigured: wwConfigured, + strategic: strategic, + consumers: consumers, + monitorMode: mode, + wwBoostDisabled: wwBoostDisabled, + tripMgr: tm, + carOptions: cars, } } @@ -132,8 +134,10 @@ type consumerView struct { Since time.Time Reason string Unconfigured bool - CanOverride bool // true for Shelly consumers (hardware read-back available) - ManualOverride bool + CanOverride bool // true for Shelly consumers (hardware read-back available) + IsWW bool // true for ConsumerWW — shows reset button instead of override + WWBoostDisabled bool // copied from pageData for template access inside range + ManualOverride bool OverrideUntil time.Time LivePowerW float64 // from Shelly PM; 0 for non-PM devices } @@ -178,6 +182,7 @@ type pageData struct { ErrMsg string DryRun bool MonitorOnly bool + WWBoostDisabled bool BatterySOC float64 GridPowerW float64 PVProductionW float64 @@ -260,6 +265,7 @@ func (s *Store) Handler() http.HandlerFunc { } monitorOnly := s.monitorMode != nil && s.monitorMode.IsActive() + wwBoostDisabled := s.wwBoostDisabled != nil && s.wwBoostDisabled.IsActive() now := time.Now() s.mu.RLock() @@ -270,6 +276,7 @@ func (s *Store) Handler() http.HandlerFunc { ErrMsg: s.errMsg, DryRun: s.dryRun, MonitorOnly: monitorOnly, + WWBoostDisabled: wwBoostDisabled, BatterySOC: s.state.BatterySOC, GridPowerW: s.state.GridPowerW, PVProductionW: s.state.PVProductionW, @@ -333,8 +340,10 @@ func (s *Store) Handler() http.HandlerFunc { Active: rec.active, Since: rec.since, Reason: rec.reason, - CanOverride: c != engine.ConsumerWW, - ManualOverride: rec.manualOverride, + CanOverride: c != engine.ConsumerWW, + IsWW: c == engine.ConsumerWW, + WWBoostDisabled: c == engine.ConsumerWW && wwBoostDisabled, + ManualOverride: rec.manualOverride, OverrideUntil: rec.overrideUntil, LivePowerW: rec.livePowerW, } @@ -895,6 +904,22 @@ h1 { font-size: 1.4rem; font-weight: 700; color: #14532d; } {{end}} {{end}} + {{if .IsWW}} +