From eaa55f72c017226756659702e3b3b3e80c62a412 Mon Sep 17 00:00:00 2001 From: SolarPower2024 Date: Sat, 26 Sep 2026 19:32:01 +0200 Subject: [PATCH 1/4] Optimizer: automatic mode passes the fork's gate In automatic mode the optimizer sets the battery mode. The fork keeps its safety limits at execution, as the one hook after upstream decided the mode: - a charge request passes the same gate as the fork's own grid charging (running peak, circuit headroom, charge power setpoint) and becomes hold when refused; so the charge power entity no longer gets 0 W while the optimizer charges - hold gives way to normal while a peak has to be covered, and peak shaving then only discharges for the peak instead of the free value - with a missing or stale optimizer result the fork's grid charging applies as without the optimizer The soc-based grid charging does not switch the battery itself while the optimizer is in control, it is an optimizer input instead. The gate is extracted from the grid charge request unchanged. Without automatic mode nothing changes (contract test). Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 75f9f3bcba7c38864ff14118f85222a7f51aa8ff) (cherry picked from commit a6333680a7a94ff111e5c063f56e06179c41bedc) --- core/site_battery.go | 1 + core/site_lm.go | 35 +++++++++-- core/site_optimizer_gate.go | 103 +++++++++++++++++++++++++++++++ core/site_optimizer_gate_test.go | 76 +++++++++++++++++++++++ core/site_peakshaving.go | 17 +++-- 5 files changed, 222 insertions(+), 10 deletions(-) create mode 100644 core/site_optimizer_gate.go create mode 100644 core/site_optimizer_gate_test.go diff --git a/core/site_battery.go b/core/site_battery.go index ccc5152f00..facd0fdf94 100644 --- a/core/site_battery.go +++ b/core/site_battery.go @@ -62,6 +62,7 @@ func (site *Site) fromTo(requested, m api.BatteryMode) bool { func (site *Site) updateBatteryMode(batteryGridChargeActive, batteryGridDischargeActive bool, rate api.Rate) { batteryMode := site.requiredBatteryMode(batteryGridChargeActive, batteryGridDischargeActive, rate) + batteryMode = site.lmGateBatteryMode(batteryMode, batteryGridChargeActive) // custom: see core/site_optimizer_lm.go // put battery into hold mode when charging is active and HEMS dimmed if dimmed := hems.Dimmed(site.hems); site.fromTo(batteryMode, api.BatteryCharge) && dimmed != nil && *dimmed { diff --git a/core/site_lm.go b/core/site_lm.go index 8bb1d652be..60e9d75edb 100644 --- a/core/site_lm.go +++ b/core/site_lm.go @@ -433,13 +433,27 @@ func (site *Site) batteryGridChargeRequested(rate api.Rate) bool { socActive := site.batterySocChargeActive() onceActive := site.batteryGridChargeOnceActive() + // the optimizer in automatic mode decides, its charge request passes the + // same gate, see site_optimizer_gate.go + if site.optimizerInControl() { + return false + } + + if !socActive && !onceActive && !site.batteryGridChargeActive(rate) { + site.releaseGridCharge() + return false + } + + return site.gridChargeGate() +} + +// gridChargeGate clears a grid charge request against a running peak and the +// circuit headroom and writes the charge power setpoint. A denied request +// releases what the battery had reserved. +func (site *Site) gridChargeGate() bool { // a running demand peak needs the battery for shaving, not charging - if !socActive && !onceActive && !site.batteryGridChargeActive(rate) || site.peakPausesGridCharge() { - // release what the battery had reserved on the circuit, lower priority - // loads would otherwise stay throttled until the reservation expires - site.lmm().Forget(site.lmBattery()) - site.writeChargeValue(0) - site.recordBatteryLimit(0, 0) + if site.peakPausesGridCharge() { + site.releaseGridCharge() return false } @@ -464,6 +478,15 @@ func (site *Site) batteryGridChargeRequested(rate api.Rate) bool { return power > 0 } +// releaseGridCharge stops grid charging: the charge power goes to 0 and what +// the battery had reserved on the circuit is released, lower priority loads +// would otherwise stay throttled until the reservation expires +func (site *Site) releaseGridCharge() { + site.lmm().Forget(site.lmBattery()) + site.writeChargeValue(0) + site.recordBatteryLimit(0, 0) +} + // recordBatteryLimit keeps what the battery may grid-charge with, which load // management compares with what it draws, see core/lm/follow.go func (site *Site) recordBatteryLimit(requested, allowed float64) { diff --git a/core/site_optimizer_gate.go b/core/site_optimizer_gate.go new file mode 100644 index 0000000000..153d4f7880 --- /dev/null +++ b/core/site_optimizer_gate.go @@ -0,0 +1,103 @@ +package core + +// Custom extension: the gate the optimizer's automatic mode passes. +// +// In automatic mode the optimizer sets the battery mode. Its charge request +// passes the same gate as the fork's own grid charging (peak, circuit, charge +// power setpoint) and becomes hold when refused. Hold gives way to normal +// while a peak has to be covered, and peak shaving then only covers peaks. +// When the optimizer result is missing or stale, the fork's grid charging +// applies as without the optimizer. Without automatic mode the battery follows +// upstream. + +import "github.com/evcc-io/evcc/api" + +// optimizerInControl reports whether the optimizer in automatic mode decides +// the battery mode right now: automatic mode with a current result +func (site *Site) optimizerInControl() bool { + if !site.Automatic() || site.GetBatteryModeExternal() != api.BatteryUnknown { + return false + } + _, ok := site.batterySuggestionMode() + return ok +} + +// optimizerCharges reports whether the optimizer in control wants grid charging +func (site *Site) optimizerCharges() bool { + if !site.optimizerInControl() { + return false + } + mode, _ := site.batterySuggestionMode() + return mode == api.BatteryCharge +} + +// optimizerHolds reports whether the optimizer in control withholds discharging +func (site *Site) optimizerHolds() bool { + if !site.optimizerInControl() { + return false + } + mode, _ := site.batterySuggestionMode() + return mode == api.BatteryHold || mode == api.BatteryHoldCharge +} + +// peakNeedsBattery reports whether the demand exceeds what may be drawn from +// the grid right now, so the battery has to discharge +func (site *Site) peakNeedsBattery() bool { + s := site.peak() + + s.mu.Lock() + defer s.mu.Unlock() + + return s.enabled && s.set != nil && s.demand > s.window.Allowed +} + +// lmGateBatteryMode passes the battery mode upstream decided in automatic mode: +// a charge request the gate refuses becomes hold, hold gives way to normal +// while a peak has to be covered, and without a current optimizer result the +// fork's grid charging applies. Unknown means no change. +func (site *Site) lmGateBatteryMode(mode api.BatteryMode, gridCharge bool) api.BatteryMode { + if !site.Automatic() || site.GetBatteryModeExternal() != api.BatteryUnknown { + return mode + } + + current := site.GetBatteryMode() + change := func(m api.BatteryMode) api.BatteryMode { + if m == current { + return api.BatteryUnknown + } + return m + } + + // missing or stale result: the fork's own grid charging, as without the optimizer + if !site.optimizerInControl() { + if gridCharge { + return change(api.BatteryCharge) + } + return mode + } + + target := mode + if target == api.BatteryUnknown { + target = current + } + + switch target { + case api.BatteryCharge: + if !site.gridChargeGate() { + site.log.DEBUG.Println("battery mode: optimizer charge refused by load management or peak shaving, holding") + return change(api.BatteryHold) + } + + case api.BatteryHold, api.BatteryHoldCharge: + site.releaseGridCharge() + if site.peakNeedsBattery() { + site.log.DEBUG.Println("battery mode: optimizer hold, covering a peak") + return change(api.BatteryNormal) + } + + default: + site.releaseGridCharge() + } + + return mode +} diff --git a/core/site_optimizer_gate_test.go b/core/site_optimizer_gate_test.go new file mode 100644 index 0000000000..15006a17bc --- /dev/null +++ b/core/site_optimizer_gate_test.go @@ -0,0 +1,76 @@ +package core + +import ( + "testing" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/core/types" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/config" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func automaticSite(t *testing.T, action string) *Site { + t.Helper() + enableAutomatic(t) + + bat := &scenarioBattery{} + site := &Site{ + log: util.NewLogger("test"), + batteryMeters: []config.Device[api.Meter]{config.NewStaticDevice(config.Named{Name: "bat"}, api.Meter(bat))}, + } + if action != "" { + site.setSuggestions(map[string]types.Suggestion{batteryKey("bat"): {Action: action}}) + } + return site +} + +// The optimizer's charge request passes the fork's gate. +func TestLmGateOptimizerCharge(t *testing.T) { + site := automaticSite(t, api.BatteryCharge.String()) + require.True(t, site.optimizerInControl()) + + assert.Equal(t, api.BatteryCharge, site.lmGateBatteryMode(api.BatteryCharge, false), "allowed") + + // a running peak refuses it + setPeakShaving(site, 7000, 30) + site.peak().demand = 9000 + assert.Equal(t, api.BatteryHold, site.lmGateBatteryMode(api.BatteryCharge, false), "held during a peak") + + // the fork's own grid charge flag does not decide while the optimizer is in control + assert.False(t, site.batteryGridChargeRequested(api.Rate{})) +} + +// Hold gives way to normal while a peak has to be covered. +func TestLmGateOptimizerHoldDuringPeak(t *testing.T) { + site := automaticSite(t, api.BatteryHold.String()) + + assert.Equal(t, api.BatteryHold, site.lmGateBatteryMode(api.BatteryHold, false), "no peak shaving") + + setPeakShaving(site, 7000, 30) + s := site.peak() + s.demand, s.window.Allowed = 6000, 7000 + assert.Equal(t, api.BatteryHold, site.lmGateBatteryMode(api.BatteryHold, false), "no peak") + assert.True(t, site.optimizerHolds()) + + s.demand = 9000 + assert.Equal(t, api.BatteryNormal, site.lmGateBatteryMode(api.BatteryHold, false), "covering a peak") +} + +// Without a current result the fork's grid charging applies as without the optimizer. +func TestLmGateOptimizerStale(t *testing.T) { + site := automaticSite(t, "") + require.False(t, site.optimizerInControl()) + + assert.Equal(t, api.BatteryCharge, site.lmGateBatteryMode(api.BatteryNormal, true)) + assert.Equal(t, api.BatteryNormal, site.lmGateBatteryMode(api.BatteryNormal, false)) +} + +// Outside automatic mode the gate passes upstream's decision unchanged. +func TestLmGateInertWithoutAutomatic(t *testing.T) { + site := &Site{log: util.NewLogger("test")} + for _, m := range []api.BatteryMode{api.BatteryUnknown, api.BatteryNormal, api.BatteryHold, api.BatteryCharge} { + assert.Equal(t, m, site.lmGateBatteryMode(m, true)) + } +} diff --git a/core/site_peakshaving.go b/core/site_peakshaving.go index 25c6f4fd4f..86a4acba73 100644 --- a/core/site_peakshaving.go +++ b/core/site_peakshaving.go @@ -632,6 +632,10 @@ func (site *Site) updatePeakShaving(state siteState) { case shaving: value = peak.Setpoint(state.gridPower, state.battery.Power, allowed) + + // the optimizer withholds discharging: only a peak is covered, see site_optimizer_gate.go + case site.optimizerHolds(): + value = peak.Setpoint(state.gridPower, state.battery.Power, allowed) } // log the start of a peak, not every cycle of it @@ -1116,11 +1120,16 @@ func (site *Site) updateBatteryModePeakAware(gridCharge, gridDischarge bool, rat site.peakCheckMeters() - if gridCharge || !site.peakShavingActive() || site.GetBatteryModeExternal() != api.BatteryUnknown { + if gridCharge || site.optimizerCharges() || !site.peakShavingActive() || site.GetBatteryModeExternal() != api.BatteryUnknown { site.updateBatteryMode(gridCharge, gridDischarge, rate) return } + // the optimizer's requests pass the gate only on the path above + if site.optimizerInControl() { + site.releaseGridCharge() + } + if site.GetBatteryMode() == api.BatteryNormal { return } @@ -1177,7 +1186,7 @@ func (site *Site) SetPeakShaving(val bool) error { if changed { settings.SetBool(keys.PeakShaving, val) site.publish(keys.PeakShaving, val) - site.Optimize() // custom: the optimizer inputs changed, see core/site_optimizer_lm.go + site.Optimize() // custom: the optimizer inputs changed, see core/site_optimizer_gate.go // the circuit may take the peak limit, see site_lm_switch.go site.applyCircuitLimits() @@ -1511,7 +1520,7 @@ func (site *Site) SetPeakShavingLimit(limit float64) error { site.log.DEBUG.Println("set peak shaving limit:", limit) settings.SetFloat(keys.PeakShavingLimit, limit) site.publish(keys.PeakShavingLimit, limit) - site.Optimize() // custom: the optimizer inputs changed, see core/site_optimizer_lm.go + site.Optimize() // custom: the optimizer inputs changed, see core/site_optimizer_gate.go // the circuit may take the peak limit, see site_lm_switch.go site.applyCircuitLimits() } @@ -1547,7 +1556,7 @@ func (site *Site) SetPeakShavingReserve(soc float64) error { site.log.DEBUG.Println("set peak shaving reserve:", soc) settings.SetFloat(keys.PeakShavingReserve, soc) site.publish(keys.PeakShavingReserve, effective) - site.Optimize() // custom: the optimizer inputs changed, see core/site_optimizer_lm.go + site.Optimize() // custom: the optimizer inputs changed, see core/site_optimizer_gate.go } return nil From fb3208c3fa8e3848ea9d16a64a2e19814bf19911 Mon Sep 17 00:00:00 2001 From: SolarPower2024 Date: Sat, 26 Sep 2026 19:32:02 +0200 Subject: [PATCH 2/4] Profiles: discharge control left to the optimizer in automatic mode A profile's discharge control is refused while the optimizer decides it; it is skipped with a debug note instead of failing the whole profile. The stored upstream value applies again once automatic mode is off. Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 7bc1bddc5fc12a28b0edc01b331bc14041b4a759) (cherry picked from commit 9739690afd6746e25a520ab1a75706bb57f5a4a9) --- core/site_lm_profiles.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/site_lm_profiles.go b/core/site_lm_profiles.go index 1a00d2b269..3cf9c46dee 100644 --- a/core/site_lm_profiles.go +++ b/core/site_lm_profiles.go @@ -132,7 +132,12 @@ func (site *Site) ApplyLmProfile(id string) error { site.applyProfileBatteryUsage(p, add) if v := p.DischargeControl; v != nil { - add("discharge control", site.SetBatteryDischargeControl(*v)) + // the optimizer decides it in automatic mode, the stored value applies again without + if err := site.SetBatteryDischargeControl(*v); errors.Is(err, ErrOptimizerAutomatic) { + site.log.DEBUG.Printf("profile %s: discharge control left to the optimizer", p.Name) + } else { + add("discharge control", err) + } } if v := p.PeakLimit; v != nil { From 84035af070d1e869c04e7422915868351d3b3fb1 Mon Sep 17 00:00:00 2001 From: SolarPower2024 Date: Sat, 26 Sep 2026 19:36:06 +0200 Subject: [PATCH 3/4] Optimizer: visible in the overview, battery page, profiles and settings - the load management overview marks loads the optimizer controls, and shows the battery as grid charging while an optimizer charge request passed the gate - battery page: with automatic mode the soc grid charging card says the optimizer plans the charging (start soc as floor, stop soc at the cheapest time) - profile editor: discharge control is skipped in automatic mode - advanced settings: "Netzlade-Ziel erreichen in" (grid charge window, 1-24 h, default 3) Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 67c2481bdbb1570cb7cac9b7106a2f5af0ead68f) (cherry picked from commit 3c0f535e5655c71fda820a118d35e9fa10e0af18) --- .../js/components/Battery/BatterySocGridChargeCard.vue | 9 +++++++++ assets/js/components/Config/LmProfilesModal.vue | 8 ++++++++ assets/js/components/LoadManagement/OverviewModal.vue | 6 ++++++ assets/js/views/Battery.vue | 1 + core/site_lm_status.go | 9 +++++++++ i18n/de.json | 10 +++++++--- i18n/en.json | 10 +++++++--- 7 files changed, 47 insertions(+), 6 deletions(-) diff --git a/assets/js/components/Battery/BatterySocGridChargeCard.vue b/assets/js/components/Battery/BatterySocGridChargeCard.vue index 0f1db3b465..8075642142 100644 --- a/assets/js/components/Battery/BatterySocGridChargeCard.vue +++ b/assets/js/components/Battery/BatterySocGridChargeCard.vue @@ -45,6 +45,13 @@ +

+ {{ $t("battery.socGridCharge.optimizer") }} +

@@ -70,6 +77,8 @@ export default defineComponent({ active: Boolean, startSoc: { type: Number, default: 20 }, stopSoc: { type: Number, default: 80 }, + // the optimizer in automatic mode plans the charging, see core/site_optimizer_gate.go + optimizer: Boolean, }, data() { return { diff --git a/assets/js/components/Config/LmProfilesModal.vue b/assets/js/components/Config/LmProfilesModal.vue index f3026795b9..dd77dda8d9 100644 --- a/assets/js/components/Config/LmProfilesModal.vue +++ b/assets/js/components/Config/LmProfilesModal.vue @@ -85,6 +85,9 @@

{{ $t("config.lmprofiles.hint") }}

+

+ {{ $t("config.lmprofiles.optimizer") }} +

{{ $t(`config.lmprofiles.group.${group.key}`) }}
@@ -182,6 +185,7 @@ import GenericModal from "../Helper/GenericModal.vue"; import ProfileIcon, { PROFILE_ICONS } from "../Battery/ProfileIcon.vue"; import store from "@/store"; +import { OPTIMIZER_AUTOMATIC } from "@/types/evcc"; import api from "@/api"; const MAX_PROFILES = 10; @@ -205,6 +209,10 @@ export default { profiles() { return store.state?.lmProfiles || []; }, + optimizerAutomatic() { + const level = store.state?.optimizerAutomatic; + return !!level && level !== OPTIMIZER_AUTOMATIC.OFF; + }, wallboxes() { return store.state?.lmProfileWallboxes || []; }, diff --git a/assets/js/components/LoadManagement/OverviewModal.vue b/assets/js/components/LoadManagement/OverviewModal.vue index 1b8ee1a74d..46c3cd6beb 100644 --- a/assets/js/components/LoadManagement/OverviewModal.vue +++ b/assets/js/components/LoadManagement/OverviewModal.vue @@ -89,6 +89,12 @@ class="evcc-gray lock" :title="$t('lmoverview.protected')" > + {{ $t("lmoverview.optimizer") }} {{ fmtW(l.power) }} diff --git a/assets/js/views/Battery.vue b/assets/js/views/Battery.vue index 54f8b5cd4e..c2b910d5de 100644 --- a/assets/js/views/Battery.vue +++ b/assets/js/views/Battery.vue @@ -47,6 +47,7 @@ :active="socGridChargeEnabled && !!state.batteryGridChargeActive" :start-soc="state.batterySocGridChargeStart ?? 20" :stop-soc="state.batterySocGridChargeStop ?? 80" + :optimizer="optimizerAutomatic" /> Date: Sat, 26 Sep 2026 20:58:51 +0200 Subject: [PATCH 4/4] docs: optimizer automatic mode gate Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 61745e05712e9fd919be60da8683cd51cffd62ae) --- core/fork_hooks_test.go | 1 + core/lm/README.md | 19 ++++++++++++++++--- core/site_battery.go | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/core/fork_hooks_test.go b/core/fork_hooks_test.go index da77ac2641..cdb91cea8a 100644 --- a/core/fork_hooks_test.go +++ b/core/fork_hooks_test.go @@ -25,6 +25,7 @@ var forkHooks = []struct { {"core/site.go", "Site.collectMeters", []string{"setPeakMeterRead"}}, {"core/site.go", "Site.update", []string{"updateCustom", "batteryGridChargeRequested", "updateBatteryModePeakAware"}}, {"core/site.go", "Site.updateLoadpoints", []string{"meteredPower"}}, + {"core/site_battery.go", "Site.updateBatteryMode", []string{"lmGateBatteryMode"}}, {"core/site_circuits.go", "Site.updateCircuits", []string{"circuitLoads"}}, {"core/site_load_predictor.go", "Site.homeProfile", []string{"homeProfileCustom"}}, {"core/site_optimizer.go", "Site.optimizerUpdateAsync", []string{"lmOptimizeLater", "lmOptimizeAgain"}}, diff --git a/core/lm/README.md b/core/lm/README.md index 5794d95c16..4d782ae54e 100644 --- a/core/lm/README.md +++ b/core/lm/README.md @@ -717,8 +717,20 @@ matches what the fork will do (`core/site_optimizer_lm.go`): forced run arriving during a run follows right after it. Without circuits, peak shaving, soc or one-time grid charging and snow on pv the -request is unchanged. The optimizer's automatic mode (evcc PR 32881) needs a gate at -execution, prepared separately on top of these inputs. +request is unchanged. + +In the optimizer's automatic mode (evcc PR 32881) it also sets the battery mode +and gates the loadpoints. The fork keeps its safety limits at execution, see +`core/site_optimizer_gate.go`: a charge request passes the same checks as the +fork's own grid charging (running peak, circuit headroom, charge power +setpoint) and becomes hold when refused; hold gives way to normal while a peak +has to be covered, and peak shaving then only covers the peak instead of +writing the free value. Below the reserve the battery stays in normal mode as +before. With a missing or stale optimizer result the fork's grid charging +applies as without the optimizer, which otherwise does not switch the battery +itself while the optimizer is in control. Profiles skip discharge control, +which the optimizer decides. The overview marks what the optimizer controls. +Without automatic mode the battery follows upstream. ### Export forecast @@ -977,6 +989,7 @@ Every change in an evcc file. Check these when merging a new evcc version. | File | Change | | --- | --- | | `core/site.go` | `custom` field; `meteredPower` in `updateLoadpoints` (an assumed heater power stays in the home consumption); `restoreCustom` deferred at the start of `restoreSettings` (after the guard for tests, so it also runs when an evcc restore step returns an error); `updateCustom` after `updatePower`; `setPeakGridEnergy` in `updateGridMeter` (with `powerRead`: whether the grid power was read, evcc keeps the old value for a meter that is not available); `setPeakMeterRead` in `collectMeters` (the result of each power read: evcc leaves a failed read at 0 W without a mark); `batteryGridChargeRequested` and `updateBatteryModePeakAware` in place of evcc's calls | +| `core/site_battery.go` | `lmGateBatteryMode` in `updateBatteryMode` after evcc decided the mode (optimizer automatic mode) | | `core/site_circuits.go` | `circuitLoads()` instead of `loadpointsAsCircuitDevices()` (adds the battery) | | `core/site_load_predictor.go` | `homeProfileCustom` call in `homeProfile` | | `core/site_optimizer.go` | `optimizerGridTariff` for the grid price, `applyLmOptimizerInputs` where the request is assembled, `lmOptimizerResult` after the solve, `lmForecastLowest` for the forecast, `lmOptimizeLater`/`lmOptimizeAgain` in `optimizerUpdateAsync` | @@ -1024,7 +1037,7 @@ Every change in an evcc file. Check these when merging a new evcc version. | EEG | `core/site_feedin_eeg.go`, `core/metrics/feedin_eeg_custom.go`, `assets/js/components/Energy/feedInEeg.ts` | | log file | `util/logstash/file_custom.go`, `core/site_logfile.go`, `assets/js/components/LogFile/` | | snow on pv | `core/site_snow.go`, `core/site_snow_auto.go`, `core/testdata/open-meteo-snow-tirol.json` (recorded answer, added with `git add -f` as `*.json` is ignored), `assets/js/components/Forecast/SnowCoverSwitch.vue` | -| optimizer | `core/site_optimizer_lm.go`, `core/site_optimizer_reserve_pass.go`, `core/site_optimizer_soc_pass.go`, `core/site_lm_export_forecast.go`, `tariff/homeassistant_forecast.go`, `templates/definition/tariff/homeassistant-forecast.yaml` | +| optimizer | `core/site_optimizer_lm.go`, `core/site_optimizer_gate.go`, `core/site_optimizer_reserve_pass.go`, `core/site_optimizer_soc_pass.go`, `core/site_lm_export_forecast.go`, `tariff/homeassistant_forecast.go`, `templates/definition/tariff/homeassistant-forecast.yaml` | | api, keys | `core/site/api_custom.go`, `server/http_custom.go`, `core/keys/site_custom.go` | | ui | `assets/js/types/evcc-lm.ts`, `assets/js/utils/lmPriorityOrder.ts`, `assets/js/utils/socSteps.ts`, `assets/js/components/LoadManagement/`, `assets/js/components/PeakShaving/`, the battery cards in `assets/js/components/Battery/` (`BatterySocGridChargeCard`, `BatteryGridChargeOnce`, `PowerIcon`, `BatteryPeakShavingCard`, `BatteryProfileCard`, `ProfileIcon`), the config components in `assets/js/components/Config/` (`PeakShavingConfig`, `LmConfigModals` and its dialogs, `FeedInEegSummary`, `PhaseSwitchFields`, `CircuitPeakHint`), `assets/js/utils/circuitFromPeak.ts` | | build | `.github/workflows/custom-image.yml` | diff --git a/core/site_battery.go b/core/site_battery.go index facd0fdf94..8a7943cc9d 100644 --- a/core/site_battery.go +++ b/core/site_battery.go @@ -62,7 +62,7 @@ func (site *Site) fromTo(requested, m api.BatteryMode) bool { func (site *Site) updateBatteryMode(batteryGridChargeActive, batteryGridDischargeActive bool, rate api.Rate) { batteryMode := site.requiredBatteryMode(batteryGridChargeActive, batteryGridDischargeActive, rate) - batteryMode = site.lmGateBatteryMode(batteryMode, batteryGridChargeActive) // custom: see core/site_optimizer_lm.go + batteryMode = site.lmGateBatteryMode(batteryMode, batteryGridChargeActive) // custom: see core/site_optimizer_gate.go // put battery into hold mode when charging is active and HEMS dimmed if dimmed := hems.Dimmed(site.hems); site.fromTo(batteryMode, api.BatteryCharge) && dimmed != nil && *dimmed {