diff --git a/assets/css/app.css b/assets/css/app.css index 3db33ddd8..83f0aa232 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -121,6 +121,18 @@ h5 { background-color: var(--evcc-dark-green) !important; } +.bg-dark-green { + background-color: var(--evcc-dark-green) !important; +} + +.bg-darker-green { + background-color: var(--evcc-darker-green) !important; +} + +.bg-darkest-green { + background-color: var(--evcc-darkest-green) !important; +} + .text-primary { color: var(--bs-primary) !important; } @@ -237,6 +249,9 @@ a:hover { .text-dark { color: var(--bs-gray-dark); } +.text-light { + color: var(--bs-gray-bright); +} .text-gray { color: var(--bs-gray-medium); } diff --git a/assets/js/components/BatterySettingsModal.vue b/assets/js/components/BatterySettingsModal.vue new file mode 100644 index 000000000..d24aa9989 --- /dev/null +++ b/assets/js/components/BatterySettingsModal.vue @@ -0,0 +1,522 @@ + + + + + diff --git a/assets/js/components/Energyflow/Energyflow.vue b/assets/js/components/Energyflow/Energyflow.vue index 9fcf7ea7b..3cd510afb 100644 --- a/assets/js/components/Energyflow/Energyflow.vue +++ b/assets/js/components/Energyflow/Energyflow.vue @@ -68,7 +68,8 @@ :soc="batterySoc" :details="batterySoc" :detailsFmt="batteryFmt" - :detailsTooltip="batteryTooltip" + detailsClickable + @details-clicked="openBatterySettingsModal" /> + @@ -163,10 +166,17 @@ import AnimatedNumber from "../AnimatedNumber.vue"; import settings from "../../settings"; import { CO2_UNIT } from "../../units"; import collector from "../../mixins/collector"; +import BatterySettingsModal from "../BatterySettingsModal.vue"; export default { name: "Energyflow", - components: { Visualization, EnergyflowEntry, AnimatedNumber, GridSettingsModal }, + components: { + Visualization, + EnergyflowEntry, + AnimatedNumber, + GridSettingsModal, + BatterySettingsModal, + }, mixins: [formatter, collector], props: { gridConfigured: Boolean, @@ -191,6 +201,9 @@ export default { smartCostLimit: { type: Number }, smartCostUnit: { type: String }, currency: { type: String }, + prioritySoc: { type: Number }, + bufferSoc: { type: Number }, + bufferStartSoc: { type: Number }, }, data: () => { return { detailsOpen: false, detailsCompleteHeight: null, gridSettingsModal: null }; @@ -234,27 +247,15 @@ export default { } return this.pv.map(({ power }) => this.fmtKw(power, this.powerInKw)); }, - batteryTooltip() { - if (!Array.isArray(this.battery)) { - return; - } - return this.battery.map(({ soc, capacity }) => { - const energy = this.fmtKWh((capacity / 100) * soc * 1e3, true, false, 1); - const total = this.fmtKWh(capacity * 1e3, true, true, 1); - const formattedSoc = this.batteryFmt(soc); - return this.$t("main.energyflow.batteryTooltip", { - energy, - total, - soc: formattedSoc, - }); - }); - }, batteryFmt() { return (soc) => `${Math.round(soc)}%`; }, gridSettings() { return this.collectProps(GridSettingsModal); }, + batterySettings() { + return this.collectProps(BatterySettingsModal); + }, co2Available() { return this.smartCostUnit === CO2_UNIT; }, @@ -263,6 +264,9 @@ export default { this.gridSettingsModal = Modal.getOrCreateInstance( document.querySelector("#gridSettingsModal") ); + this.batterySettingsModal = Modal.getOrCreateInstance( + document.querySelector("#batterySettingsModal") + ); window.addEventListener("resize", this.updateHeight); // height must be calculated in case of initially open details if (settings.energyflowDetails) { @@ -309,6 +313,9 @@ export default { openGridSettingsModal() { this.gridSettingsModal.show(); }, + openBatterySettingsModal() { + this.batterySettingsModal.show(); + }, }, }; diff --git a/assets/js/components/Site.vue b/assets/js/components/Site.vue index 038584725..3ee219689 100644 --- a/assets/js/components/Site.vue +++ b/assets/js/components/Site.vue @@ -70,6 +70,8 @@ export default { battery: Array, gridCurrents: Array, prioritySoc: Number, + bufferSoc: Number, + bufferStartSoc: Number, siteTitle: String, vehicles: Array, diff --git a/core/loadpoint.go b/core/loadpoint.go index e2e6dd63f..4629a0963 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -1070,7 +1070,7 @@ func (lp *Loadpoint) publishTimer(name string, delay time.Duration, action strin } // pvMaxCurrent calculates the maximum target current for PV mode -func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batteryBuffered bool) float64 { +func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batteryBuffered, batteryStart bool) float64 { // read only once to simplify testing minCurrent := lp.GetMinCurrent() maxCurrent := lp.GetMaxCurrent() @@ -1427,7 +1427,7 @@ func (lp *Loadpoint) guardGracePeriodElapsed() bool { } // Update is the main control function. It reevaluates meters and charger state -func (lp *Loadpoint) Update(sitePower float64, autoCharge, batteryBuffered bool, greenShare float64, effPrice, effCo2 *float64) { +func (lp *Loadpoint) Update(sitePower float64, autoCharge, batteryBuffered, batteryStart bool, greenShare float64, effPrice, effCo2 *float64) { lp.processTasks() mode := lp.GetMode() @@ -1526,7 +1526,7 @@ func (lp *Loadpoint) Update(sitePower float64, autoCharge, batteryBuffered bool, break } - targetCurrent := lp.pvMaxCurrent(mode, sitePower, batteryBuffered) + targetCurrent := lp.pvMaxCurrent(mode, sitePower, batteryBuffered, batteryStart) var required bool // false if targetCurrent == 0 && lp.vehicleClimateActive() { diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go index 2f252ee5a..bb9774368 100644 --- a/core/loadpoint_test.go +++ b/core/loadpoint_test.go @@ -182,7 +182,7 @@ func TestUpdatePowerZero(t *testing.T) { } lp.Mode = tc.mode - lp.Update(0, false, false, 0, nil, nil) // sitePower false,0 + lp.Update(0, false, false, false, 0, nil, nil) // false,sitePower false,0 ctrl.Finish() } @@ -338,7 +338,7 @@ func TestPVHysteresis(t *testing.T) { // charger.EXPECT().Enabled().Return(tc.enabled, nil) lp.enabled = tc.enabled - current := lp.pvMaxCurrent(api.ModePV, se.site, false) + current := lp.pvMaxCurrent(api.ModePV, se.site, false, false) if current != se.current { t.Errorf("step %d: wanted %.1f, got %.1f", step, se.current, current) @@ -371,7 +371,7 @@ func TestPVHysteresisForStatusOtherThanC(t *testing.T) { // maxCurrent will read enabled state in PV mode sitePower := -float64(phases)*minA*Voltage + 1 // 1W below min power - current := lp.pvMaxCurrent(api.ModePV, sitePower, false) + current := lp.pvMaxCurrent(api.ModePV, sitePower, false, false) if current != 0 { t.Errorf("PV mode could not disable charger as expected. Expected 0, got %.f", current) @@ -428,7 +428,7 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) { charger.EXPECT().Status().Return(api.StatusC, nil) charger.EXPECT().Enabled().Return(lp.enabled, nil) charger.EXPECT().MaxCurrent(int64(maxA)).Return(nil) - lp.Update(500, false, false, 0, nil, nil) + lp.Update(500, false, false, false, 0, nil, nil) t.Log("charging above target - soc deactivates charger") clock.Add(5 * time.Minute) @@ -436,20 +436,20 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) { charger.EXPECT().Status().Return(api.StatusC, nil) charger.EXPECT().Enabled().Return(lp.enabled, nil) charger.EXPECT().Enable(false).Return(nil) - lp.Update(500, false, false, 0, nil, nil) + lp.Update(500, false, false, false, 0, nil, nil) t.Log("deactivated charger changes status to B") clock.Add(5 * time.Minute) vehicle.EXPECT().Soc().Return(95.0, nil) charger.EXPECT().Status().Return(api.StatusB, nil) charger.EXPECT().Enabled().Return(lp.enabled, nil) - lp.Update(-5000, false, false, 0, nil, nil) + lp.Update(-5000, false, false, false, 0, nil, nil) t.Log("soc has fallen below target - soc update prevented by timer") clock.Add(5 * time.Minute) charger.EXPECT().Status().Return(api.StatusB, nil) charger.EXPECT().Enabled().Return(lp.enabled, nil) - lp.Update(-5000, false, false, 0, nil, nil) + lp.Update(-5000, false, false, false, 0, nil, nil) t.Log("soc has fallen below target - soc update timer expired") clock.Add(pollInterval) @@ -457,7 +457,7 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) { charger.EXPECT().Status().Return(api.StatusB, nil) charger.EXPECT().Enabled().Return(lp.enabled, nil) charger.EXPECT().Enable(true).Return(nil) - lp.Update(-5000, false, false, 0, nil, nil) + lp.Update(-5000, false, false, false, 0, nil, nil) ctrl.Finish() } @@ -498,14 +498,14 @@ func TestSetModeAndSocAtDisconnect(t *testing.T) { charger.EXPECT().Enabled().Return(lp.enabled, nil) charger.EXPECT().Status().Return(api.StatusC, nil) charger.EXPECT().MaxCurrent(int64(maxA)).Return(nil) - lp.Update(500, false, false, 0, nil, nil) + lp.Update(500, false, false, false, 0, nil, nil) t.Log("switch off when disconnected") clock.Add(5 * time.Minute) charger.EXPECT().Enabled().Return(lp.enabled, nil) charger.EXPECT().Status().Return(api.StatusA, nil) charger.EXPECT().Enable(false).Return(nil) - lp.Update(-3000, false, false, 0, nil, nil) + lp.Update(-3000, false, false, false, 0, nil, nil) if lp.Mode != api.ModeOff { t.Error("unexpected mode", lp.Mode) @@ -568,14 +568,14 @@ func TestChargedEnergyAtDisconnect(t *testing.T) { rater.EXPECT().ChargedEnergy().Return(0.0, nil) charger.EXPECT().Enabled().Return(lp.enabled, nil) charger.EXPECT().Status().Return(api.StatusC, nil) - lp.Update(-1, false, false, 0, nil, nil) + lp.Update(-1, false, false, false, 0, nil, nil) t.Log("at 1:00h charging at 5 kWh") clock.Add(time.Hour) rater.EXPECT().ChargedEnergy().Return(5.0, nil) charger.EXPECT().Enabled().Return(lp.enabled, nil) charger.EXPECT().Status().Return(api.StatusC, nil) - lp.Update(-1, false, false, 0, nil, nil) + lp.Update(-1, false, false, false, 0, nil, nil) expectCache("chargedEnergy", 5000.0) t.Log("at 1:00h stop charging at 5 kWh") @@ -583,7 +583,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) { rater.EXPECT().ChargedEnergy().Return(5.0, nil) charger.EXPECT().Enabled().Return(lp.enabled, nil) charger.EXPECT().Status().Return(api.StatusB, nil) - lp.Update(-1, false, false, 0, nil, nil) + lp.Update(-1, false, false, false, 0, nil, nil) expectCache("chargedEnergy", 5000.0) t.Log("at 1:00h restart charging at 5 kWh") @@ -591,7 +591,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) { rater.EXPECT().ChargedEnergy().Return(5.0, nil) charger.EXPECT().Enabled().Return(lp.enabled, nil) charger.EXPECT().Status().Return(api.StatusC, nil) - lp.Update(-1, false, false, 0, nil, nil) + lp.Update(-1, false, false, false, 0, nil, nil) expectCache("chargedEnergy", 5000.0) t.Log("at 1:30h continue charging at 7.5 kWh") @@ -599,7 +599,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) { rater.EXPECT().ChargedEnergy().Return(7.5, nil) charger.EXPECT().Enabled().Return(lp.enabled, nil) charger.EXPECT().Status().Return(api.StatusC, nil) - lp.Update(-1, false, false, 0, nil, nil) + lp.Update(-1, false, false, false, 0, nil, nil) expectCache("chargedEnergy", 7500.0) t.Log("at 2:00h stop charging at 10 kWh") @@ -607,7 +607,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) { rater.EXPECT().ChargedEnergy().Return(10.0, nil) charger.EXPECT().Enabled().Return(lp.enabled, nil) charger.EXPECT().Status().Return(api.StatusB, nil) - lp.Update(-1, false, false, 0, nil, nil) + lp.Update(-1, false, false, false, 0, nil, nil) expectCache("chargedEnergy", 10000.0) ctrl.Finish() diff --git a/core/loadpoint_vehicle_test.go b/core/loadpoint_vehicle_test.go index b3209f804..8d72dd9ed 100644 --- a/core/loadpoint_vehicle_test.go +++ b/core/loadpoint_vehicle_test.go @@ -361,7 +361,7 @@ func TestReconnectVehicle(t *testing.T) { // vehicle not updated yet vehicle.MockChargeState.EXPECT().Status().Return(api.StatusA, nil) - lp.Update(0, false, false, 0, nil, nil) + lp.Update(0, false, false, false, 0, nil, nil) ctrl.Finish() // detection started @@ -375,7 +375,7 @@ func TestReconnectVehicle(t *testing.T) { // vehicle not updated yet vehicle.MockChargeState.EXPECT().Status().Return(api.StatusB, nil) - lp.Update(0, false, false, 0, nil, nil) + lp.Update(0, false, false, false, 0, nil, nil) ctrl.Finish() // vehicle detected diff --git a/core/site.go b/core/site.go index a84cb145a..3e71e65f6 100644 --- a/core/site.go +++ b/core/site.go @@ -17,6 +17,7 @@ import ( "github.com/evcc-io/evcc/core/prioritizer" "github.com/evcc-io/evcc/push" serverdb "github.com/evcc-io/evcc/server/db" + "github.com/evcc-io/evcc/server/db/settings" "github.com/evcc-io/evcc/tariff" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/telemetry" @@ -27,7 +28,7 @@ const standbyPower = 10 // consider less than 10W as charger in standby // Updater abstracts the Loadpoint implementation for testing type Updater interface { loadpoint.API - Update(availablePower float64, autoCharge, batteryBuffered bool, greenShare float64, effectivePrice *float64, effectiveCo2 *float64) + Update(availablePower float64, autoCharge, batteryBuffered, batteryStart bool, greenShare float64, effectivePrice, effectiveCo2 *float64) } // meterMeasurement is used as slice element for publishing structured data @@ -58,7 +59,8 @@ type Site struct { ResidualPower float64 `mapstructure:"residualPower"` // PV meter only: household usage. Grid meter: household safety margin Meters MetersConfig // Meter references PrioritySoc float64 `mapstructure:"prioritySoc"` // prefer battery up to this Soc - BufferSoc float64 `mapstructure:"bufferSoc"` // ignore battery above this Soc + BufferSoc float64 `mapstructure:"bufferSoc"` // continue charging on battery above this Soc + BufferStartSoc float64 `mapstructure:"bufferStartSoc"` // start charging on battery above this Soc MaxGridSupplyWhileBatteryCharging float64 `mapstructure:"maxGridSupplyWhileBatteryCharging"` // ignore battery charging if AC consumption is above this value SmartCostLimit float64 `mapstructure:"smartCostLimit"` // always charge if cost is below this value @@ -114,6 +116,8 @@ func NewSiteFromConfig( site.prioritizer = prioritizer.New() site.savings = NewSavings(tariffs) + site.restoreSettings() + // upload telemetry on shutdown if telemetry.Enabled() { shutdown.Register(func() { @@ -179,6 +183,14 @@ func NewSiteFromConfig( return nil, errors.New("missing either grid or pv meter") } + if site.BufferStartSoc <= site.BufferSoc { + site.log.WARN.Println("bufferStartSoc must be larger than bufferSoc") + } + + if site.PrioritySoc > site.BufferSoc { + site.log.WARN.Println("bufferSoc must be larger than prioritySoc") + } + return site, nil } @@ -202,6 +214,21 @@ func (site *Site) Loadpoints() []loadpoint.API { return res } +func (site *Site) restoreSettings() { + if v, err := settings.Float("site.bufferSoc"); err == nil { + site.BufferSoc = v + } + if v, err := settings.Float("site.bufferStartSoc"); err == nil { + site.BufferStartSoc = v + } + if v, err := settings.Float("site.prioritySoc"); err == nil { + site.PrioritySoc = v + } + if v, err := settings.Float("site.smartCostLimit"); err == nil { + site.SmartCostLimit = v + } +} + func meterCapabilities(name string, meter interface{}) string { _, power := meter.(api.Meter) _, energy := meter.(api.MeterEnergy) @@ -491,9 +518,9 @@ func (site *Site) updateMeters() error { // - the net power exported by the site minus a residual margin // (negative values mean grid: export, battery: charging // - if battery buffer can be used for charging -func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, bool, error) { +func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, bool, bool, error) { if err := site.updateMeters(); err != nil { - return 0, false, err + return 0, false, false, err } // allow using PV as estimate for grid power @@ -515,7 +542,7 @@ func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, b batteryPower := site.batteryPower // handed to loadpoint - var batteryBuffered bool + var batteryBuffered, batteryStart bool if len(site.batteryMeters) > 0 { site.Lock() @@ -528,6 +555,7 @@ func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, b } else { // if battery is above bufferSoc allow using it for charging batteryBuffered = site.BufferSoc > 0 && site.batterySoc > site.BufferSoc + batteryStart = site.BufferStartSoc > 0 && site.batterySoc > site.BufferStartSoc } } @@ -564,7 +592,7 @@ func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, b site.log.DEBUG.Printf("site power: %.0fW", sitePower) - return sitePower, batteryBuffered, nil + return sitePower, batteryBuffered, batteryStart, nil } func (site *Site) greenShare() float64 { @@ -663,9 +691,9 @@ func (site *Site) update(lp Updater) { } } - if sitePower, batteryBuffered, err := site.sitePower(totalChargePower, flexiblePower); err == nil { + if sitePower, batteryBuffered, batteryStart, err := site.sitePower(totalChargePower, flexiblePower); err == nil { greenShare := site.greenShare() - lp.Update(sitePower, autoCharge, batteryBuffered, greenShare, site.effectivePrice(greenShare), site.effectiveCo2(greenShare)) + lp.Update(sitePower, autoCharge, batteryBuffered, batteryStart, greenShare, site.effectivePrice(greenShare), site.effectiveCo2(greenShare)) // ignore negative pvPower values as that means it is not an energy source but consumption homePower := site.gridPower + math.Max(0, site.pvPower) + site.batteryPower - totalChargePower @@ -693,12 +721,16 @@ func (site *Site) prepare() { site.publish("pvConfigured", len(site.pvMeters) > 0) site.publish("batteryConfigured", len(site.batteryMeters) > 0) site.publish("bufferSoc", site.BufferSoc) + site.publish("bufferStartSoc", site.BufferStartSoc) site.publish("prioritySoc", site.PrioritySoc) site.publish("residualPower", site.ResidualPower) site.publish("smartCostLimit", site.SmartCostLimit) if tariff := site.GetTariff(PlannerTariff); tariff != nil { site.publish("smartCostUnit", tariff.Unit()) site.publish("smartCostAvailable", tariff.IsDynamic()) + } else { + site.publish("smartCostUnit", nil) + site.publish("smartCostAvailable", nil) } site.publish("currency", site.tariffs.Currency.String()) site.publish("savingsSince", site.savings.Since()) diff --git a/core/site/api.go b/core/site/api.go index 5b2dc358f..217ff69d9 100644 --- a/core/site/api.go +++ b/core/site/api.go @@ -16,6 +16,8 @@ type API interface { GetBufferSoc() float64 SetBufferSoc(float64) error + GetBufferStartSoc() float64 + SetBufferStartSoc(float64) error GetPrioritySoc() float64 SetPrioritySoc(float64) error diff --git a/core/site_api.go b/core/site_api.go index ce20fe5f9..b634fdcd8 100644 --- a/core/site_api.go +++ b/core/site_api.go @@ -5,6 +5,7 @@ import ( "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/core/site" + "github.com/evcc-io/evcc/server/db/settings" ) var _ site.API = (*Site)(nil) @@ -32,6 +33,7 @@ func (site *Site) SetPrioritySoc(soc float64) error { } site.PrioritySoc = soc + settings.SetFloat("site.prioritySoc", site.PrioritySoc) site.publish("prioritySoc", site.PrioritySoc) return nil @@ -54,11 +56,35 @@ func (site *Site) SetBufferSoc(soc float64) error { } site.BufferSoc = soc + settings.SetFloat("site.bufferSoc", site.BufferSoc) site.publish("bufferSoc", site.BufferSoc) return nil } +// GetBufferStartSoc returns the BufferStartSoc +func (site *Site) GetBufferStartSoc() float64 { + site.Lock() + defer site.Unlock() + return site.BufferStartSoc +} + +// SetBufferStartSoc sets the BufferStartSoc +func (site *Site) SetBufferStartSoc(soc float64) error { + site.Lock() + defer site.Unlock() + + if len(site.batteryMeters) == 0 { + return errors.New("battery not configured") + } + + site.BufferStartSoc = soc + settings.SetFloat("site.bufferStartSoc", site.BufferStartSoc) + site.publish("bufferStartSoc", site.BufferStartSoc) + + return nil +} + // GetResidualPower returns the ResidualPower func (site *Site) GetResidualPower() float64 { site.Lock() @@ -90,6 +116,7 @@ func (site *Site) SetSmartCostLimit(val float64) error { defer site.Unlock() site.SmartCostLimit = val + settings.SetFloat("site.smartCostLimit", site.SmartCostLimit) site.publish("smartCostLimit", site.SmartCostLimit) return nil diff --git a/evcc.dist.yaml b/evcc.dist.yaml index d60093cd5..4f536c0dc 100644 --- a/evcc.dist.yaml +++ b/evcc.dist.yaml @@ -109,11 +109,12 @@ site: - battery # list of battery meters aux: - aux # list of auxiliary meters for adjusting grid operating point - residualPower: # additional household usage margin - prioritySoc: # give home battery priority up to this soc (empty to disable) - bufferSoc: # ignore home battery discharge above soc (empty to disable) - maxGridSupplyWhileBatteryCharging: # ignore battery charging if AC consumption is above this value - smartCostLimit: # set cost limit for automatic charging in PV mode + residualPower: 0 # additional household usage margin + prioritySoc: 0 # give home battery priority up to this soc (empty to disable) + bufferSoc: 0 # continue charging on battery above soc (0 to disable) + bufferStartSoc: 0 # start charging on battery above soc (0 to disable) + maxGridSupplyWhileBatteryCharging: 0 # ignore battery charging if AC consumption is above this value + smartCostLimit: 0 # set cost limit for automatic charging in PV mode # loadpoint describes the charger, charge meter and connected vehicle loadpoints: diff --git a/i18n/de.toml b/i18n/de.toml index 5da23add8..133df37c4 100644 --- a/i18n/de.toml +++ b/i18n/de.toml @@ -1,3 +1,23 @@ +[batterySettings] +batteryLevel = "Ladestand" +capacity = "{energy} von {total}" +legendBottomName = "Haus hat Priorität" +legendBottomSubline = "nicht zum Laden verwenden" +legendMiddleName = "Fahrzeug zuerst" +legendMiddleSubline = "dann das Haus" +legendTitle = "Wie soll Sonnenstrom genutzt werden?" +legendTopAutostart = "startet automatisch" +legendTopName = "batterieunterstütztes Laden" +legendTopSubline = "ohne Unterbrechungen" +modalTitle = "Batterie Einstellungen" +note = "Hinweis: Nur relevant im PV Modus. Das Ladeverhalten wird entsprechend angepasst." + +[batterySettings.bufferStart] +full = "wenn fast voll" +low = "wenn etwas voll" +medium = "wenn halbvoll" +never = "nur mit genug PV-Überschuss" + [footer] [footer.community] diff --git a/i18n/en.toml b/i18n/en.toml index dd8a15338..195681e21 100644 --- a/i18n/en.toml +++ b/i18n/en.toml @@ -1,3 +1,23 @@ +[batterySettings] +batteryLevel = "Battery level" +capacity = "{energy} of {total}" +legendBottomName = "home priority" +legendBottomSubline = "not used for charging" +legendMiddleName = "vehicle first" +legendMiddleSubline = "home second" +legendTitle = "How should solar energy be used?" +legendTopAutostart = "starts automatically" +legendTopName = "battery supported charging" +legendTopSubline = "without interruptions" +modalTitle = "Battery settings" +note = "Note: These settings only affect the solar mode. Charging behaviour is adjusted accordingly." + +[batterySettings.bufferStart] +full = "when nearly full" +low = "when somewhat full" +medium = "when half full" +never = "only with enough surplus" + [footer] [footer.community] diff --git a/schema.json b/schema.json index 8e126002e..ff2c8619e 100644 --- a/schema.json +++ b/schema.json @@ -209,6 +209,9 @@ "bufferSoc": { "type": "number" }, + "bufferStartSoc": { + "type": "number" + }, "maxGridSupplyWhileBatteryCharging": { "type": "number" }, diff --git a/server/http.go b/server/http.go index 956dd4cbd..2add27147 100644 --- a/server/http.go +++ b/server/http.go @@ -95,21 +95,22 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, cache *util.Cache) { // site api routes := map[string]route{ - "health": {[]string{"GET"}, "/health", healthHandler(site)}, - "state": {[]string{"GET"}, "/state", stateHandler(cache)}, - "config": {[]string{"GET"}, "/config/templates/{class:[a-z]+}", templatesHandler}, - "products": {[]string{"GET"}, "/config/products/{class:[a-z]+}", productsHandler}, - "test": {[]string{"POST"}, "/config/test/{class:[a-z]+}", testHandler}, - "buffersoc": {[]string{"POST", "OPTIONS"}, "/buffersoc/{value:[0-9.]+}", floatHandler(site.SetBufferSoc, site.GetBufferSoc)}, - "prioritysoc": {[]string{"POST", "OPTIONS"}, "/prioritysoc/{value:[0-9.]+}", floatHandler(site.SetPrioritySoc, site.GetPrioritySoc)}, - "residualpower": {[]string{"POST", "OPTIONS"}, "/residualpower/{value:[-0-9.]+}", floatHandler(site.SetResidualPower, site.GetResidualPower)}, - "smartcost": {[]string{"POST", "OPTIONS"}, "/smartcostlimit/{value:[-0-9.]+}", floatHandler(site.SetSmartCostLimit, site.GetSmartCostLimit)}, - "tariff": {[]string{"GET"}, "/tariff/{tariff:[a-z]+}", tariffHandler(site)}, - "sessions": {[]string{"GET"}, "/sessions", sessionHandler}, - "session1": {[]string{"PUT"}, "/session/{id:[0-9]+}", updateSessionHandler}, - "session2": {[]string{"DELETE"}, "/session/{id:[0-9]+}", deleteSessionHandler}, - "telemetry": {[]string{"GET"}, "/settings/telemetry", boolGetHandler(telemetry.Enabled)}, - "telemetry2": {[]string{"POST", "OPTIONS"}, "/settings/telemetry/{value:[a-z]+}", boolHandler(telemetry.Enable, telemetry.Enabled)}, + "health": {[]string{"GET"}, "/health", healthHandler(site)}, + "state": {[]string{"GET"}, "/state", stateHandler(cache)}, + "config": {[]string{"GET"}, "/config/templates/{class:[a-z]+}", templatesHandler}, + "products": {[]string{"GET"}, "/config/products/{class:[a-z]+}", productsHandler}, + "test": {[]string{"POST"}, "/config/test/{class:[a-z]+}", testHandler}, + "buffersoc": {[]string{"POST", "OPTIONS"}, "/buffersoc/{value:[0-9.]+}", floatHandler(site.SetBufferSoc, site.GetBufferSoc)}, + "bufferstartsoc": {[]string{"POST", "OPTIONS"}, "/bufferstartsoc/{value:[0-9.]+}", floatHandler(site.SetBufferStartSoc, site.GetBufferStartSoc)}, + "prioritysoc": {[]string{"POST", "OPTIONS"}, "/prioritysoc/{value:[0-9.]+}", floatHandler(site.SetPrioritySoc, site.GetPrioritySoc)}, + "residualpower": {[]string{"POST", "OPTIONS"}, "/residualpower/{value:[-0-9.]+}", floatHandler(site.SetResidualPower, site.GetResidualPower)}, + "smartcost": {[]string{"POST", "OPTIONS"}, "/smartcostlimit/{value:[-0-9.]+}", floatHandler(site.SetSmartCostLimit, site.GetSmartCostLimit)}, + "tariff": {[]string{"GET"}, "/tariff/{tariff:[a-z]+}", tariffHandler(site)}, + "sessions": {[]string{"GET"}, "/sessions", sessionHandler}, + "session1": {[]string{"PUT"}, "/session/{id:[0-9]+}", updateSessionHandler}, + "session2": {[]string{"DELETE"}, "/session/{id:[0-9]+}", deleteSessionHandler}, + "telemetry": {[]string{"GET"}, "/settings/telemetry", boolGetHandler(telemetry.Enabled)}, + "telemetry2": {[]string{"POST", "OPTIONS"}, "/settings/telemetry/{value:[a-z]+}", boolHandler(telemetry.Enable, telemetry.Enabled)}, } for _, r := range routes { diff --git a/server/mqtt.go b/server/mqtt.go index 346e5efb3..829e2638c 100644 --- a/server/mqtt.go +++ b/server/mqtt.go @@ -197,6 +197,12 @@ func (m *MQTT) Run(site site.API, in <-chan util.Param) { } }) + m.Handler.ListenSetter(fmt.Sprintf("%s/site/bufferStartSoc/set", m.root), func(payload string) { + if val, err := strconv.ParseFloat(payload, 64); err == nil { + _ = site.SetBufferStartSoc(val) + } + }) + m.Handler.ListenSetter(fmt.Sprintf("%s/site/residualPower/set", m.root), func(payload string) { if val, err := strconv.ParseFloat(payload, 64); err == nil { _ = site.SetResidualPower(val)