From ebbcabb83883f903f94ef8cf5ae379c877638785 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Wed, 18 Jun 2025 21:36:49 +0200 Subject: [PATCH] Add min/max physical current to loadpoint (#21187) --- .../js/components/Config/LoadpointModal.vue | 82 +++-- assets/js/components/Loadpoints/Loadpoint.vue | 2 + .../components/Loadpoints/SettingsModal.vue | 316 ++++++++---------- assets/js/mixins/formatter.ts | 2 +- core/keys/loadpoint.go | 42 +-- core/loadpoint.go | 22 +- core/loadpoint/api.go | 6 + core/loadpoint/config.go | 29 +- core/loadpoint/mock.go | 42 +++ core/loadpoint_api.go | 82 +++++ core/loadpoint_current_test.go | 130 +++++++ core/prioritizer/prioritizer_test.go | 2 + i18n/cs.json | 8 +- i18n/da.json | 8 +- i18n/de.json | 8 +- i18n/el.json | 8 +- i18n/en.json | 15 +- i18n/fi.json | 8 +- i18n/fr.json | 8 +- i18n/hr.json | 8 +- i18n/hu.json | 8 +- i18n/it.json | 8 +- i18n/lb.json | 8 +- i18n/lt.json | 8 +- i18n/nl.json | 8 +- i18n/pt.json | 8 +- i18n/sl.json | 8 +- i18n/sv.json | 8 +- i18n/tr.json | 8 +- i18n/uk.json | 8 +- server/http_config_loadpoint_handler.go | 30 +- tests/basics.spec.ts | 15 + tests/config-loadpoint.spec.ts | 85 +++++ 33 files changed, 696 insertions(+), 342 deletions(-) create mode 100644 core/loadpoint_current_test.go diff --git a/assets/js/components/Config/LoadpointModal.vue b/assets/js/components/Config/LoadpointModal.vue index bd165e89a..a03672323 100644 --- a/assets/js/components/Config/LoadpointModal.vue +++ b/assets/js/components/Config/LoadpointModal.vue @@ -308,13 +308,7 @@ diff --git a/assets/js/components/Loadpoints/Loadpoint.vue b/assets/js/components/Loadpoints/Loadpoint.vue index 14360c279..9861b1115 100644 --- a/assets/js/components/Loadpoints/Loadpoint.vue +++ b/assets/js/components/Loadpoints/Loadpoint.vue @@ -190,6 +190,8 @@ export default defineComponent({ chargerSinglePhase: Boolean, minCurrent: Number, maxCurrent: Number, + minPhysicalCurrent: Number, + maxPhysicalCurrent: Number, offeredCurrent: Number, connectedDuration: Number, chargeCurrents: Array, diff --git a/assets/js/components/Loadpoints/SettingsModal.vue b/assets/js/components/Loadpoints/SettingsModal.vue index ab560ef51..4beae4d53 100644 --- a/assets/js/components/Loadpoints/SettingsModal.vue +++ b/assets/js/components/Loadpoints/SettingsModal.vue @@ -1,159 +1,117 @@ diff --git a/assets/js/mixins/formatter.ts b/assets/js/mixins/formatter.ts index ab720a671..b05f1e3b1 100644 --- a/assets/js/mixins/formatter.ts +++ b/assets/js/mixins/formatter.ts @@ -90,7 +90,7 @@ export default defineComponent({ fmtWh(watt: number, format = POWER_UNIT.KW, withUnit = true, digits?: number) { return this.fmtW(watt, format, withUnit, digits) + (withUnit ? "h" : ""); }, - fmtNumber(number: number, decimals: number, unit?: string) { + fmtNumber(number: number, decimals?: number, unit?: string) { const style = unit ? "unit" : "decimal"; return new Intl.NumberFormat(this.$i18n?.locale, { style, diff --git a/core/keys/loadpoint.go b/core/keys/loadpoint.go index 76620aac0..a06732f6e 100644 --- a/core/keys/loadpoint.go +++ b/core/keys/loadpoint.go @@ -2,26 +2,28 @@ package keys const ( // loadpoint settings - Title = "title" // loadpoint title - Mode = "mode" // charge mode - DefaultMode = "defaultMode" // default charge mode - Charger = "charger" // charger ref - Meter = "meter" // meter ref - Circuit = "circuit" // circuit ref - DefaultVehicle = "vehicle" // default vehicle ref - Priority = "priority" // priority - MinCurrent = "minCurrent" // min current - MaxCurrent = "maxCurrent" // max current - MinSoc = "minSoc" // min soc - LimitSoc = "limitSoc" // limit soc - LimitEnergy = "limitEnergy" // limit energy - Soc = "soc" - Thresholds = "thresholds" - EnableThreshold = "enableThreshold" - DisableThreshold = "disableThreshold" - EnableDelay = "enableDelay" - DisableDelay = "disableDelay" - BatteryBoost = "batteryBoost" + Title = "title" // loadpoint title + Mode = "mode" // charge mode + DefaultMode = "defaultMode" // default charge mode + Charger = "charger" // charger ref + Meter = "meter" // meter ref + Circuit = "circuit" // circuit ref + DefaultVehicle = "vehicle" // default vehicle ref + Priority = "priority" // priority + MinCurrent = "minCurrent" // min current + MaxCurrent = "maxCurrent" // max current + MinPhysicalCurrent = "minPhysicalCurrent" // min physical current + MaxPhysicalCurrent = "maxPhysicalCurrent" // max physical current + MinSoc = "minSoc" // min soc + LimitSoc = "limitSoc" // limit soc + LimitEnergy = "limitEnergy" // limit energy + Soc = "soc" + Thresholds = "thresholds" + EnableThreshold = "enableThreshold" + DisableThreshold = "disableThreshold" + EnableDelay = "enableDelay" + DisableDelay = "disableDelay" + BatteryBoost = "batteryBoost" PhasesConfigured = "phasesConfigured" // desired phase mode (0/1/3, 0 = automatic), user selection PhasesActive = "phasesActive" // expectedly active phases, taking vehicle into account (1/2/3) diff --git a/core/loadpoint.go b/core/loadpoint.go index 4192e334b..3ddeaf974 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -107,15 +107,17 @@ type Loadpoint struct { MinCurrent_ float64 `mapstructure:"minCurrent"` // ignored, present for compatibility MaxCurrent_ float64 `mapstructure:"maxCurrent"` // ignored, present for compatibility - title string // UI title - priority int // Priority - minCurrent float64 // PV mode: start current Min+PV mode: min current - maxCurrent float64 // Max allowed current. Physically ensured by the charger - phasesConfigured int // Charger configured phase mode 0/1/3 - limitSoc int // Session limit for soc - limitEnergy float64 // Session limit for energy - smartCostLimit *float64 // always charge if cost is below this value - batteryBoost int // battery boost state + title string // UI title + priority int // Priority + minCurrent float64 // PV mode: start current Min+PV mode: min current + maxCurrent float64 // Max allowed current. Physically ensured by the charger + minPhysicalCurrent *float64 // Min physical current + maxPhysicalCurrent *float64 // Max physical current + phasesConfigured int // Charger configured phase mode 0/1/3 + limitSoc int // Session limit for soc + limitEnergy float64 // Session limit for energy + smartCostLimit *float64 // always charge if cost is below this value + batteryBoost int // battery boost state mode api.ChargeMode enabled bool // Charger enabled state @@ -613,6 +615,8 @@ func (lp *Loadpoint) Prepare(site site.API, uiChan chan<- util.Param, pushChan c lp.publish(keys.Priority, lp.GetPriority()) lp.publish(keys.MinCurrent, lp.GetMinCurrent()) lp.publish(keys.MaxCurrent, lp.GetMaxCurrent()) + lp.publish(keys.MinPhysicalCurrent, lp.GetMinPhysicalCurrent()) + lp.publish(keys.MaxPhysicalCurrent, lp.GetMaxPhysicalCurrent()) lp.publish(keys.EnableThreshold, lp.Enable.Threshold) lp.publish(keys.DisableThreshold, lp.Disable.Threshold) diff --git a/core/loadpoint/api.go b/core/loadpoint/api.go index 6908ecbbe..587311a06 100644 --- a/core/loadpoint/api.go +++ b/core/loadpoint/api.go @@ -65,6 +65,12 @@ type API interface { GetMaxCurrent() float64 // SetMaxCurrent sets the max charging current SetMaxCurrent(float64) error + // GetMinPhysicalCurrent returns the min physical charging current + GetMinPhysicalCurrent() *float64 + // GetMaxPhysicalCurrent returns the max physical charging current + GetMaxPhysicalCurrent() *float64 + // SetPhysicalCurrentRange sets min/max physical current + SetPhysicalCurrentRange(*float64, *float64) error // GetMode returns the current charge mode GetMode() api.ChargeMode diff --git a/core/loadpoint/config.go b/core/loadpoint/config.go index 6357ea3d2..20a5c7adc 100644 --- a/core/loadpoint/config.go +++ b/core/loadpoint/config.go @@ -17,18 +17,20 @@ type StaticConfig struct { type DynamicConfig struct { // dynamic config - Title string `json:"title"` - DefaultMode string `json:"defaultMode"` - Priority int `json:"priority"` - PhasesConfigured int `json:"phasesConfigured"` - MinCurrent float64 `json:"minCurrent"` - MaxCurrent float64 `json:"maxCurrent"` - SmartCostLimit *float64 `json:"smartCostLimit"` - PlanEnergy float64 `json:"planEnergy"` - PlanTime time.Time `json:"planTime"` - PlanPrecondition int64 `json:"planPrecondition"` - LimitEnergy float64 `json:"limitEnergy"` - LimitSoc int `json:"limitSoc"` + Title string `json:"title"` + DefaultMode string `json:"defaultMode"` + Priority int `json:"priority"` + PhasesConfigured int `json:"phasesConfigured"` + MinCurrent float64 `json:"minCurrent"` + MaxCurrent float64 `json:"maxCurrent"` + MinPhysicalCurrent *float64 `json:"minPhysicalCurrent"` + MaxPhysicalCurrent *float64 `json:"maxPhysicalCurrent"` + SmartCostLimit *float64 `json:"smartCostLimit"` + PlanEnergy float64 `json:"planEnergy"` + PlanTime time.Time `json:"planTime"` + PlanPrecondition int64 `json:"planPrecondition"` + LimitEnergy float64 `json:"limitEnergy"` + LimitSoc int `json:"limitSoc"` Thresholds ThresholdsConfig `json:"thresholds"` Soc SocConfig `json:"soc"` @@ -73,6 +75,9 @@ func (payload DynamicConfig) Apply(lp API) error { err = lp.SetPhasesConfigured(payload.PhasesConfigured) } + if err == nil { + err = lp.SetPhysicalCurrentRange(payload.MinPhysicalCurrent, payload.MaxPhysicalCurrent) + } if err == nil && payload.MinCurrent != 0 { err = lp.SetMinCurrent(payload.MinCurrent) } diff --git a/core/loadpoint/mock.go b/core/loadpoint/mock.go index e098a02cd..f4f322648 100644 --- a/core/loadpoint/mock.go +++ b/core/loadpoint/mock.go @@ -349,6 +349,20 @@ func (mr *MockAPIMockRecorder) GetMaxPhaseCurrent() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaxPhaseCurrent", reflect.TypeOf((*MockAPI)(nil).GetMaxPhaseCurrent)) } +// GetMaxPhysicalCurrent mocks base method. +func (m *MockAPI) GetMaxPhysicalCurrent() *float64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMaxPhysicalCurrent") + ret0, _ := ret[0].(*float64) + return ret0 +} + +// GetMaxPhysicalCurrent indicates an expected call of GetMaxPhysicalCurrent. +func (mr *MockAPIMockRecorder) GetMaxPhysicalCurrent() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaxPhysicalCurrent", reflect.TypeOf((*MockAPI)(nil).GetMaxPhysicalCurrent)) +} + // GetMeterRef mocks base method. func (m *MockAPI) GetMeterRef() string { m.ctrl.T.Helper() @@ -377,6 +391,20 @@ func (mr *MockAPIMockRecorder) GetMinCurrent() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMinCurrent", reflect.TypeOf((*MockAPI)(nil).GetMinCurrent)) } +// GetMinPhysicalCurrent mocks base method. +func (m *MockAPI) GetMinPhysicalCurrent() *float64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMinPhysicalCurrent") + ret0, _ := ret[0].(*float64) + return ret0 +} + +// GetMinPhysicalCurrent indicates an expected call of GetMinPhysicalCurrent. +func (mr *MockAPIMockRecorder) GetMinPhysicalCurrent() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMinPhysicalCurrent", reflect.TypeOf((*MockAPI)(nil).GetMinPhysicalCurrent)) +} + // GetMode mocks base method. func (m *MockAPI) GetMode() api.ChargeMode { m.ctrl.T.Helper() @@ -870,6 +898,20 @@ func (mr *MockAPIMockRecorder) SetPhasesConfigured(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPhasesConfigured", reflect.TypeOf((*MockAPI)(nil).SetPhasesConfigured), arg0) } +// SetPhysicalCurrentRange mocks base method. +func (m *MockAPI) SetPhysicalCurrentRange(arg0, arg1 *float64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetPhysicalCurrentRange", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetPhysicalCurrentRange indicates an expected call of SetPhysicalCurrentRange. +func (mr *MockAPIMockRecorder) SetPhysicalCurrentRange(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPhysicalCurrentRange", reflect.TypeOf((*MockAPI)(nil).SetPhysicalCurrentRange), arg0, arg1) +} + // SetPlanEnergy mocks base method. func (m *MockAPI) SetPlanEnergy(arg0 time.Time, arg1 time.Duration, arg2 float64) error { m.ctrl.T.Helper() diff --git a/core/loadpoint_api.go b/core/loadpoint_api.go index 049021ca5..5983dd3d2 100644 --- a/core/loadpoint_api.go +++ b/core/loadpoint_api.go @@ -654,6 +654,10 @@ func (lp *Loadpoint) SetMinCurrent(current float64) error { lp.Lock() defer lp.Unlock() + if lp.minPhysicalCurrent != nil && current < *lp.minPhysicalCurrent { + return fmt.Errorf("min current must be greater or equal than min physical current: %.1fA", *lp.minPhysicalCurrent) + } + if current > lp.maxCurrent { return errors.New("min current must be smaller or equal than max current") } @@ -690,6 +694,10 @@ func (lp *Loadpoint) SetMaxCurrent(current float64) error { lp.Lock() defer lp.Unlock() + if lp.maxPhysicalCurrent != nil && current > *lp.maxPhysicalCurrent { + return fmt.Errorf("max current must be smaller or equal than max physical current: %.1fA", *lp.maxPhysicalCurrent) + } + if current < lp.minCurrent { return errors.New("max current must be greater or equal than min current") } @@ -702,6 +710,80 @@ func (lp *Loadpoint) SetMaxCurrent(current float64) error { return nil } +// GetMinPhysicalCurrent returns the min physical current +func (lp *Loadpoint) GetMinPhysicalCurrent() *float64 { + lp.RLock() + defer lp.RUnlock() + return lp.getMinPhysicalCurrent() +} + +// getMinPhysicalCurrent returns the min physical current +func (lp *Loadpoint) getMinPhysicalCurrent() *float64 { + return lp.minPhysicalCurrent +} + +// setMinPhysicalCurrent sets the min physical current (no mutex) +func (lp *Loadpoint) setMinPhysicalCurrent(current *float64) { + lp.minPhysicalCurrent = current + lp.publish(keys.MinPhysicalCurrent, *current) + lp.settings.SetFloatPtr(keys.MinPhysicalCurrent, current) +} + +// GetMaxPhysicalCurrent returns the max physical current +func (lp *Loadpoint) GetMaxPhysicalCurrent() *float64 { + lp.RLock() + defer lp.RUnlock() + return lp.getMaxPhysicalCurrent() +} + +// getMaxPhysicalCurrent returns the max physical current +func (lp *Loadpoint) getMaxPhysicalCurrent() *float64 { + return lp.maxPhysicalCurrent +} + +// setMaxPhysicalCurrent sets the max physical current (no mutex) +func (lp *Loadpoint) setMaxPhysicalCurrent(current *float64) { + lp.maxPhysicalCurrent = current + lp.publish(keys.MaxPhysicalCurrent, *current) + lp.settings.SetFloatPtr(keys.MaxPhysicalCurrent, current) +} + +// SetPhysicalCurrentRange sets min/max physical current +func (lp *Loadpoint) SetPhysicalCurrentRange(min, max *float64) error { + lp.Lock() + defer lp.Unlock() + + if min != nil && max != nil && *min > *max { + return fmt.Errorf("min physical current must be smaller or equal than max physical current: %.1fA", *max) + } + + lp.log.DEBUG.Println("set physical current range:", printPtr("%.1f", min), printPtr("%.1f", max)) + lp.setMinPhysicalCurrent(min) + lp.setMaxPhysicalCurrent(max) + + // adjust to min limit + if min != nil && lp.minCurrent < *min { + lp.log.DEBUG.Println("adjust min current to comply with min physical current:", *min) + lp.setMinCurrent(*min) + } + if min != nil && lp.maxCurrent < *min { + lp.log.DEBUG.Println("adjust max current to comply with min physical current:", *min) + lp.setMaxCurrent(*min) + } + + // adjust to max limit + if max != nil && lp.maxCurrent > *max { + lp.log.DEBUG.Println("adjust max current to comply with max physical current:", *max) + lp.setMaxCurrent(*max) + } + if max != nil && lp.minCurrent > *max { + lp.log.DEBUG.Println("adjust min current to comply with max physical current:", *max) + lp.setMinCurrent(*max) + } + + return nil +} + // IsFastChargingActive indicates if fast charging with maximum power is active func (lp *Loadpoint) IsFastChargingActive() bool { lp.RLock() diff --git a/core/loadpoint_current_test.go b/core/loadpoint_current_test.go new file mode 100644 index 000000000..212140de9 --- /dev/null +++ b/core/loadpoint_current_test.go @@ -0,0 +1,130 @@ +package core + +import ( + "testing" + + evbus "github.com/asaskevich/EventBus" + "github.com/benbjohnson/clock" + "github.com/evcc-io/evcc/core/settings" + "github.com/evcc-io/evcc/util" + "github.com/stretchr/testify/assert" +) + +func TestSetPhysicalCurrentRange(t *testing.T) { + lp := &Loadpoint{ + log: util.NewLogger("test"), + clock: clock.New(), + bus: evbus.New(), + settings: settings.NewDatabaseSettingsAdapter("foo"), + minCurrent: 6, + maxCurrent: 16, + } + + // Test with valid range + min, max := 5.0, 32.0 + err := lp.SetPhysicalCurrentRange(&min, &max) + assert.NoError(t, err) + assert.Equal(t, min, *lp.getMinPhysicalCurrent()) + assert.Equal(t, max, *lp.getMaxPhysicalCurrent()) + assert.Equal(t, 6.0, lp.minCurrent) // No adjustment needed + assert.Equal(t, 16.0, lp.maxCurrent) // No adjustment needed + + // Test with invalid range (min > max) + min, max = 20.0, 15.0 + err = lp.SetPhysicalCurrentRange(&min, &max) + assert.Error(t, err) + + // Test with min adjustment needed + min, max = 10.0, 32.0 + lp.minCurrent = 6 + lp.maxCurrent = 16 + err = lp.SetPhysicalCurrentRange(&min, &max) + assert.NoError(t, err) + assert.Equal(t, 10.0, lp.minCurrent) // Should be adjusted to minPhysical + + // Test with max adjustment needed + min, max = 5.0, 15.0 + lp.minCurrent = 6 + lp.maxCurrent = 16 + err = lp.SetPhysicalCurrentRange(&min, &max) + assert.NoError(t, err) + assert.Equal(t, 15.0, lp.maxCurrent) // Should be adjusted to maxPhysical +} + +func TestSetMinMaxCurrentWithPhysicalLimits(t *testing.T) { + lp := &Loadpoint{ + log: util.NewLogger("test"), + clock: clock.New(), + bus: evbus.New(), + settings: settings.NewDatabaseSettingsAdapter("foo"), + minCurrent: 6, + maxCurrent: 16, + } + + // Set physical limits + min, max := 5.0, 32.0 + err := lp.SetPhysicalCurrentRange(&min, &max) + assert.NoError(t, err) + + // Test valid min current + err = lp.SetMinCurrent(10.0) + assert.NoError(t, err) + assert.Equal(t, 10.0, lp.minCurrent) + + // Test min current below physical min + err = lp.SetMinCurrent(4.0) + assert.Error(t, err) + + // Test valid max current + err = lp.SetMaxCurrent(20.0) + assert.NoError(t, err) + assert.Equal(t, 20.0, lp.maxCurrent) + + // Test max current above physical max + err = lp.SetMaxCurrent(40.0) + assert.Error(t, err) + + // Test min current > max current + lp.maxCurrent = 15.0 + err = lp.SetMinCurrent(16.0) + assert.Error(t, err) + + // Test max current < min current + lp.minCurrent = 10.0 + err = lp.SetMaxCurrent(9.0) + assert.Error(t, err) +} + +func TestPhysicalCurrentRangeAdjustment(t *testing.T) { + lp := &Loadpoint{ + log: util.NewLogger("test"), + clock: clock.New(), + bus: evbus.New(), + settings: settings.NewDatabaseSettingsAdapter("foo"), + } + + tests := []struct { + minPhysical, maxPhysical float64 + min, max, minExpected, maxExpected float64 + }{ + {5, 32, 10, 16, 10, 16}, // No adjustment needed + {10, 32, 6, 16, 10, 16}, // Min current below physical min + {5, 16, 6, 32, 6, 16}, // Max current above physical max + {10, 20, 6, 32, 10, 20}, // Both currents outside physical range + {5, 16, 20, 32, 16, 16}, // Min current > max physical current + {10, 32, 6, 8, 10, 10}, // Max current < min physical current + } + + for _, tc := range tests { + lp.minCurrent = tc.min + lp.maxCurrent = tc.max + + minPhysical, maxPhysical := tc.minPhysical, tc.maxPhysical + + err := lp.SetPhysicalCurrentRange(&minPhysical, &maxPhysical) + + assert.NoError(t, err) + assert.Equal(t, tc.minExpected, lp.minCurrent) + assert.Equal(t, tc.maxExpected, lp.maxCurrent) + } +} diff --git a/core/prioritizer/prioritizer_test.go b/core/prioritizer/prioritizer_test.go index 0d65c87a8..2c518bf29 100644 --- a/core/prioritizer/prioritizer_test.go +++ b/core/prioritizer/prioritizer_test.go @@ -17,11 +17,13 @@ func TestPrioritzer(t *testing.T) { lo.EXPECT().GetTitle().AnyTimes() lo.EXPECT().GetPriority().Return(0).AnyTimes() // prio 0 lo.EXPECT().EffectivePriority().Return(0).AnyTimes() // prio 0 + lo.EXPECT().GetMaxPhysicalCurrent().Return((*float64)(nil)).AnyTimes() hi := loadpoint.NewMockAPI(ctrl) hi.EXPECT().GetTitle().AnyTimes() hi.EXPECT().GetPriority().Return(1).AnyTimes() // prio 1 hi.EXPECT().EffectivePriority().Return(1).AnyTimes() // prio 1 + hi.EXPECT().GetMaxPhysicalCurrent().Return((*float64)(nil)).AnyTimes() // no additional power available lo.EXPECT().GetChargePowerFlexibility(nil).Return(300.0) diff --git a/i18n/cs.json b/i18n/cs.json index 49d7d5061..e16460311 100644 --- a/i18n/cs.json +++ b/i18n/cs.json @@ -204,10 +204,10 @@ "energyMeterHelp": "Doplňkový měřič, pokud nabíječka nemá integrovaný.", "energyMeterLabel": "Elektroměr", "estimateLabel": "Odhadovat úroveň nabití vozu mezi aktualizacemi z API", - "maxCurrentHelp": "Musí být větší než minimální proud.", - "maxCurrentLabel": "Maximální proud", - "minCurrentHelp": "Pod 6 A jděte pouze tehdy, pokud víte, co děláte.", - "minCurrentLabel": "Minimální proud", + "maxPhysicalCurrentHelp": "Musí být větší než minimální proud.", + "maxPhysicalCurrentLabel": "Maximální proud", + "minPhysicalCurrentHelp": "Pod 6 A jděte pouze tehdy, pokud víte, co děláte.", + "minPhysicalCurrentLabel": "Minimální proud", "noVehicles": "Není nakonfigurováno žádné vozidlo.", "phases1p": "1 fáze", "phases3p": "3 fáze", diff --git a/i18n/da.json b/i18n/da.json index 16f464dc5..796eff5db 100644 --- a/i18n/da.json +++ b/i18n/da.json @@ -205,10 +205,10 @@ "energyMeterHelp": "Ekstra måler, hvis opladeren ikke har en integreret.", "energyMeterLabel": "Energi måler", "estimateLabel": "Interpoler opladningsniveauet mellem API-opdateringer", - "maxCurrentHelp": "Skal være større end minimum strømstyrken.", - "maxCurrentLabel": "Maksimal strømstyrke", - "minCurrentHelp": "Brug kun lavere end 6 A, hvis du er sikker på hvad du gør.", - "minCurrentLabel": "Minimum strømstyrke", + "maxPhysicalCurrentHelp": "Skal være større end minimum strømstyrken.", + "maxPhysicalCurrentLabel": "Maksimal strømstyrke", + "minPhysicalCurrentHelp": "Brug kun lavere end 6 A, hvis du er sikker på hvad du gør.", + "minPhysicalCurrentLabel": "Minimum strømstyrke", "noVehicles": "Ingen køretøjer er konfigureret.", "phases1p": "1-fase", "phases3p": "3-faser", diff --git a/i18n/de.json b/i18n/de.json index 815015a3d..b31e18b2a 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -205,10 +205,10 @@ "energyMeterHelp": "Zusätzlicher Zähler, wenn die Wallbox keinen integrierten hat.", "energyMeterLabel": "Energiezähler", "estimateLabel": "Ladestand zwischen API-Updates interpolieren", - "maxCurrentHelp": "Muss größer als der minimale Strom sein.", - "maxCurrentLabel": "Maximaler Strom", - "minCurrentHelp": "Gehe nur unter 6 A, wenn du weißt, was du tust.", - "minCurrentLabel": "Minimaler Strom", + "maxPhysicalCurrentHelp": "Muss größer als der minimale Strom sein.", + "maxPhysicalCurrentLabel": "Maximaler Strom", + "minPhysicalCurrentHelp": "Warnung: 6 A ist der im IEC Standard vorgeschriebene Mindestwert für das Laden von Elektrofahrzeugen.", + "minPhysicalCurrentLabel": "Minimaler Strom", "noVehicles": "Keine Fahrzeuge konfiguriert.", "phases1p": "1-phasig", "phases3p": "3-phasig", diff --git a/i18n/el.json b/i18n/el.json index 7b13a0745..4c0c4b464 100644 --- a/i18n/el.json +++ b/i18n/el.json @@ -205,10 +205,10 @@ "energyMeterHelp": "Πρόσθετος μετρητής εάν ο φορτιστής δεν έχει ενσωματωμένο.", "energyMeterLabel": "Μετρητής ενέργειας", "estimateLabel": "Παρεμβολή επιπέδου φόρτισης μεταξύ ενημερώσεων API", - "maxCurrentHelp": "Πρέπει να είναι μεγαλύτερο από την ελάχιστη ένταση.", - "maxCurrentLabel": "Μέγιστη ένταση", - "minCurrentHelp": "Πηγαίνετε κάτω από 6A μόνο εάν ξέρετε τι κάνετε.", - "minCurrentLabel": "Ελάχιστη ένταση", + "maxPhysicalCurrentHelp": "Πρέπει να είναι μεγαλύτερο από την ελάχιστη ένταση.", + "maxPhysicalCurrentLabel": "Μέγιστη ένταση", + "minPhysicalCurrentHelp": "Πηγαίνετε κάτω από 6A μόνο εάν ξέρετε τι κάνετε.", + "minPhysicalCurrentLabel": "Ελάχιστη ένταση", "noVehicles": "Δεν έχουν προστεθεί οχήματα.", "phases1p": "μονοφασικό", "phases3p": "τριφασικό", diff --git a/i18n/en.json b/i18n/en.json index 4f65ef223..7b0ec97b9 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -186,11 +186,12 @@ "chargerError": "Configuring a charger is required.", "chargerLabel": "Charger", "chargerPower11kw": "11 kW", - "chargerPower11kwHelp": "Will use a current range of 6 to 16 A.", + "chargerPower11kwHelp": "Use 6 to 16 A.", "chargerPower22kw": "22 kW", - "chargerPower22kwHelp": "Will use a current range of 6 to 32 A.", + "chargerPower22kwHelp": "Use 6 to 32 A.", "chargerPowerCustom": "other", - "chargerPowerCustomHelp": "Define a custom current range.", + "chargerPowerCustomHelp": "Custom current range.", + "chargerPowerHelp": "Allowed current range. Limits the user-selectable options in UI and API.", "chargerTypeLabel": "Charger type", "chargingTitle": "Charging", "circuitHelp": "Load management assignment to ensure power and current limits are not exceeded.", @@ -205,10 +206,10 @@ "energyMeterHelp": "Additional meter if the charger doesn't have an integrated one.", "energyMeterLabel": "Energy meter", "estimateLabel": "Interpolate charge level between API updates", - "maxCurrentHelp": "Must be greater than minimum current.", - "maxCurrentLabel": "Maximum current", - "minCurrentHelp": "Only go below 6 A if you know what you're doing.", - "minCurrentLabel": "Minimum current", + "maxPhysicalCurrentHelp": "Must be greater than minimum current.", + "maxPhysicalCurrentLabel": "Maximum current", + "minPhysicalCurrentHelp": "Warning: 6 A is the minimum value defined by the IEC standard for EV charging.", + "minPhysicalCurrentLabel": "Minimum current", "noVehicles": "No vehicles are configured.", "phases1p": "1-phase", "phases3p": "3-phase", diff --git a/i18n/fi.json b/i18n/fi.json index d1c39683a..91cff99fb 100644 --- a/i18n/fi.json +++ b/i18n/fi.json @@ -190,10 +190,10 @@ "energyMeterHelp": "Lisämittari, jos laturissa ei ole integroitua energiamittaria.", "energyMeterLabel": "Energiamittari", "estimateLabel": "Arvioi lataustaso API-päivitysten välillä", - "maxCurrentHelp": "On oltava suurempi kuin minimivirta.", - "maxCurrentLabel": "Maksimivirta", - "minCurrentHelp": "Valitse alle 6 A vain, jos tiedät mitä olet tekemässä.", - "minCurrentLabel": "Minimivirta", + "maxPhysicalCurrentHelp": "On oltava suurempi kuin minimivirta.", + "maxPhysicalCurrentLabel": "Maksimivirta", + "minPhysicalCurrentHelp": "Valitse alle 6 A vain, jos tiedät mitä olet tekemässä.", + "minPhysicalCurrentLabel": "Minimivirta", "noVehicles": "Ajoneuvoja ei ole määritetty.", "phases1p": "1-vaihe", "phases3p": "3-vaihe", diff --git a/i18n/fr.json b/i18n/fr.json index 0d48d136c..106f1e46c 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -205,10 +205,10 @@ "energyMeterHelp": "Compteur supplémentaire si le chargeur n’en a pas un intégré.", "energyMeterLabel": "Compteur d’énergie", "estimateLabel": "Interpoler le niveau de charge entre deux mises à jour via l’API", - "maxCurrentHelp": "Doit être supérieur au courant minimum.", - "maxCurrentLabel": "Courant maximum", - "minCurrentHelp": "Allez en dessous de 6 A seulement si vous savez ce que vous faîtes.", - "minCurrentLabel": "Courant minimum", + "maxPhysicalCurrentHelp": "Doit être supérieur au courant minimum.", + "maxPhysicalCurrentLabel": "Courant maximum", + "minPhysicalCurrentHelp": "Allez en dessous de 6 A seulement si vous savez ce que vous faîtes.", + "minPhysicalCurrentLabel": "Courant minimum", "noVehicles": "Aucun véhicule n’est configuré.", "phases1p": "Monophasé", "phases3p": "Triphasé", diff --git a/i18n/hr.json b/i18n/hr.json index ba38f9a4f..dffbbad6b 100644 --- a/i18n/hr.json +++ b/i18n/hr.json @@ -205,10 +205,10 @@ "energyMeterHelp": "Dodatni strujomjer ako punjač nema integrirani.", "energyMeterLabel": "Strujomjer", "estimateLabel": "Intepolirati razine punjenja između API očitavanja", - "maxCurrentHelp": "Mora biti veće od minimalne struje.", - "maxCurrentLabel": "Maksimalna struja", - "minCurrentHelp": "Postavite ispod 6A samo ako znate što radite.", - "minCurrentLabel": "Minimalna struja", + "maxPhysicalCurrentHelp": "Mora biti veće od minimalne struje.", + "maxPhysicalCurrentLabel": "Maksimalna struja", + "minPhysicalCurrentHelp": "Postavite ispod 6A samo ako znate što radite.", + "minPhysicalCurrentLabel": "Minimalna struja", "noVehicles": "Niti jedno vozilo nije postavljeno.", "phases1p": "1 faza", "phases3p": "3 faze", diff --git a/i18n/hu.json b/i18n/hu.json index c3872436e..717a521f4 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -205,10 +205,10 @@ "energyMeterHelp": "További fogyasztásmérő, ha a töltő nem rendelkezik egy integrálttal.", "energyMeterLabel": "Fogyasztásmérő", "estimateLabel": "Interpolálja a töltési szintet az API frissítések között", - "maxCurrentHelp": "Nagyobbnak kell lennie mint a minimális áram.", - "maxCurrentLabel": "Maximum áram", - "minCurrentHelp": "Csak akkor menj 6 A alá, ha tudod, hogy mit csinálsz.", - "minCurrentLabel": "Minimum áram", + "maxPhysicalCurrentHelp": "Nagyobbnak kell lennie mint a minimális áram.", + "maxPhysicalCurrentLabel": "Maximum áram", + "minPhysicalCurrentHelp": "Csak akkor menj 6 A alá, ha tudod, hogy mit csinálsz.", + "minPhysicalCurrentLabel": "Minimum áram", "noVehicles": "Nincs jármű konfigurálva.", "phases1p": "1-fázis", "phases3p": "3-fázis", diff --git a/i18n/it.json b/i18n/it.json index 5a6d9e5f4..01f2cf5d2 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -196,10 +196,10 @@ "energyMeterHelp": "Contatore supplementare se il caricabatterie non ne possiede uno integrato.", "energyMeterLabel": "Misuratore di energia", "estimateLabel": "Stima il livello di carica tra gli aggiornamenti dell'API", - "maxCurrentHelp": "Deve essere maggiore della corrente minima.", - "maxCurrentLabel": "Corrente massima", - "minCurrentHelp": "Scendere al di sotto dei 6A solo se si sa quello che si sta facendo.", - "minCurrentLabel": "Corrente minima", + "maxPhysicalCurrentHelp": "Deve essere maggiore della corrente minima.", + "maxPhysicalCurrentLabel": "Corrente massima", + "minPhysicalCurrentHelp": "Scendere al di sotto dei 6A solo se si sa quello che si sta facendo.", + "minPhysicalCurrentLabel": "Corrente minima", "noVehicles": "Nessun veicolo configurato.", "phases1p": "Monofase", "phases3p": "Trifase", diff --git a/i18n/lb.json b/i18n/lb.json index f917bd49e..839adc31f 100644 --- a/i18n/lb.json +++ b/i18n/lb.json @@ -205,10 +205,10 @@ "energyMeterHelp": "Zousätzlechen Zieler, falls d'Wallbox keen integréierten huet.", "energyMeterLabel": "Energiezieler", "estimateLabel": "Luedstand zwëschen API-Updates interpoléieren", - "maxCurrentHelp": "Muss méi grouss wéi de minimale Stroum sinn.", - "maxCurrentLabel": "Maximale Stroum", - "minCurrentHelp": "Géi nëmmen ënner 6 A, wann s du weess, wat s du mëss.", - "minCurrentLabel": "Minimale Stroum", + "maxPhysicalCurrentHelp": "Muss méi grouss wéi de minimale Stroum sinn.", + "maxPhysicalCurrentLabel": "Maximale Stroum", + "minPhysicalCurrentHelp": "Géi nëmmen ënner 6 A, wann s du weess, wat s du mëss.", + "minPhysicalCurrentLabel": "Minimale Stroum", "noVehicles": "Kee Gefier konfiguréiert.", "phases1p": "1-phaseg", "phases3p": "3-phaseg", diff --git a/i18n/lt.json b/i18n/lt.json index fe5e9050d..cd253d528 100644 --- a/i18n/lt.json +++ b/i18n/lt.json @@ -205,10 +205,10 @@ "energyMeterHelp": "Papildomas skaitiklis, jei įkroviklyje nėra integruoto.", "energyMeterLabel": "Energijos skaitiklis", "estimateLabel": "Interpoliuoti įkrovos lygį tarp API naujinimų", - "maxCurrentHelp": "Turi būti didesnė nei minimali srovė.", - "maxCurrentLabel": "Max srovė", - "minCurrentHelp": "Tik jei žinote, ką darote, leiskite žemiau 6 A.", - "minCurrentLabel": "Min srovė", + "maxPhysicalCurrentHelp": "Turi būti didesnė nei minimali srovė.", + "maxPhysicalCurrentLabel": "Max srovė", + "minPhysicalCurrentHelp": "Tik jei žinote, ką darote, leiskite žemiau 6 A.", + "minPhysicalCurrentLabel": "Min srovė", "noVehicles": "Nėra sukonfigūruotų automobilių.", "phases1p": "1-fazė", "phases3p": "3-fazės", diff --git a/i18n/nl.json b/i18n/nl.json index b577498ad..96576ef4e 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -205,10 +205,10 @@ "energyMeterHelp": "Extra meter voor laadstations zonder geïntegreerde meter.", "energyMeterLabel": "Energiemeter", "estimateLabel": "“Interpoleer batterijpercentage tussen API updates”", - "maxCurrentHelp": "Moet groter zijn dan de minimum stroom.", - "maxCurrentLabel": "“Maximale stroom”", - "minCurrentHelp": "Kies 6 A enkel wanneer je weet wat je aan het doen bent.", - "minCurrentLabel": "“Minimale stroom”", + "maxPhysicalCurrentHelp": "Moet groter zijn dan de minimum stroom.", + "maxPhysicalCurrentLabel": "“Maximale stroom”", + "minPhysicalCurrentHelp": "Kies 6 A enkel wanneer je weet wat je aan het doen bent.", + "minPhysicalCurrentLabel": "“Minimale stroom”", "noVehicles": "Geen voertuigen ingesteld.", "phases1p": "“1-fase”", "phases3p": "“3-fase”", diff --git a/i18n/pt.json b/i18n/pt.json index 46f740b58..615e3a59c 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -205,10 +205,10 @@ "energyMeterHelp": "Contador adicional se o Carregador não tiver um integrado.", "energyMeterLabel": "Contador de energia", "estimateLabel": "Interpolar o nível de carga entre actualizações da API", - "maxCurrentHelp": "Deve ser superior à Corrente mínima.", - "maxCurrentLabel": "Corrente máxima", - "minCurrentHelp": "Defina apenas abaixo dos 6 A, se souber o que está a fazer.", - "minCurrentLabel": "Corrente mínima", + "maxPhysicalCurrentHelp": "Deve ser superior à Corrente mínima.", + "maxPhysicalCurrentLabel": "Corrente máxima", + "minPhysicalCurrentHelp": "Defina apenas abaixo dos 6 A, se souber o que está a fazer.", + "minPhysicalCurrentLabel": "Corrente mínima", "noVehicles": "Nenhum veículo configurado.", "phases1p": "Monofásico", "phases3p": "Trifásico", diff --git a/i18n/sl.json b/i18n/sl.json index 8bca6aee6..fcb5ab0c2 100644 --- a/i18n/sl.json +++ b/i18n/sl.json @@ -197,10 +197,10 @@ "energyMeterHelp": "Dodatni števec, če polnilec nima vgrajenega.", "energyMeterLabel": "Energijski števec", "estimateLabel": "Interpoliraj raven zaračunavanja med posodobitvami API-ja", - "maxCurrentHelp": "Mora biti večji od minimalnega toka.", - "maxCurrentLabel": "Maksimalni tok", - "minCurrentHelp": "Pod 6 A pojdi samo, če veš, kaj delaš.", - "minCurrentLabel": "Minimalni tok", + "maxPhysicalCurrentHelp": "Mora biti večji od minimalnega toka.", + "maxPhysicalCurrentLabel": "Maksimalni tok", + "minPhysicalCurrentHelp": "Pod 6 A pojdi samo, če veš, kaj delaš.", + "minPhysicalCurrentLabel": "Minimalni tok", "noVehicles": "Nobeno vozilo ni konfigurirano.", "phases1p": "1-fazni", "phases3p": "3-fazni", diff --git a/i18n/sv.json b/i18n/sv.json index 5522b8838..b966f2598 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -205,10 +205,10 @@ "energyMeterHelp": "Extern mätare (Om laddaren inte har en integrerad mätare).", "energyMeterLabel": "Energimätare", "estimateLabel": "Interpolera laddningsnivå mellan API uppdateringar", - "maxCurrentHelp": "Måste vara högre än minimi strömstyrka.", - "maxCurrentLabel": "Maximal strömstyrka", - "minCurrentHelp": "Ange endast under 6 A om du vet vad du gör.", - "minCurrentLabel": "Minimum strömstyrka", + "maxPhysicalCurrentHelp": "Måste vara högre än minimi strömstyrka.", + "maxPhysicalCurrentLabel": "Maximal strömstyrka", + "minPhysicalCurrentHelp": "Ange endast under 6 A om du vet vad du gör.", + "minPhysicalCurrentLabel": "Minimum strömstyrka", "noVehicles": "Inga fordon är konfigurerade.", "phases1p": "1-fas", "phases3p": "3-fas", diff --git a/i18n/tr.json b/i18n/tr.json index fa4892406..6c680606e 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -205,10 +205,10 @@ "energyMeterHelp": "Doldurma cihazında bütünleşik bir sayaç yoksa ek sayaç.", "energyMeterLabel": "“Enerji sayacı”", "estimateLabel": "“API güncellemeleri arasında doldurma seviyesini hesapla”", - "maxCurrentHelp": "Asgari akımdan büyük olmalı.", - "maxCurrentLabel": "“Azami akım”", - "minCurrentHelp": "Sadece ne yaptığını biliyorsan 6 A altına in.", - "minCurrentLabel": "“Asgari akım”", + "maxPhysicalCurrentHelp": "Asgari akımdan büyük olmalı.", + "maxPhysicalCurrentLabel": "“Azami akım”", + "minPhysicalCurrentHelp": "Sadece ne yaptığını biliyorsan 6 A altına in.", + "minPhysicalCurrentLabel": "“Asgari akım”", "noVehicles": "Hiç araç yapılandırılmamış.", "phases1p": "“1 aşamalı”", "phases3p": "“3 aşamalı”", diff --git a/i18n/uk.json b/i18n/uk.json index 2a1d8c347..e2b812795 100644 --- a/i18n/uk.json +++ b/i18n/uk.json @@ -205,10 +205,10 @@ "energyMeterHelp": "Додатковий лічильник, якщо зарядний пристрій не має вбудованого.", "energyMeterLabel": "Енерголічильник", "estimateLabel": "Інтерполювати рівень оплати між оновленнями API", - "maxCurrentHelp": "Повинен бути більшим за мінімальний струм.", - "maxCurrentLabel": "Максимальний струм", - "minCurrentHelp": "Опускайтеся нижче 6 А, лише якщо знаєте, що робите.", - "minCurrentLabel": "Мінімальний струм", + "maxPhysicalCurrentHelp": "Повинен бути більшим за мінімальний струм.", + "maxPhysicalCurrentLabel": "Максимальний струм", + "minPhysicalCurrentHelp": "Опускайтеся нижче 6 А, лише якщо знаєте, що робите.", + "minPhysicalCurrentLabel": "Мінімальний струм", "noVehicles": "Транспортні засоби не налаштовані.", "phases1p": "1-фаза", "phases3p": "3-фазний", diff --git a/server/http_config_loadpoint_handler.go b/server/http_config_loadpoint_handler.go index 5957b495d..ccfc4b153 100644 --- a/server/http_config_loadpoint_handler.go +++ b/server/http_config_loadpoint_handler.go @@ -29,20 +29,22 @@ func getLoadpointStaticConfig(lp loadpoint.API) loadpoint.StaticConfig { func getLoadpointDynamicConfig(lp loadpoint.API) loadpoint.DynamicConfig { planTime, planPrecondition, planEnergy := lp.GetPlanEnergy() return loadpoint.DynamicConfig{ - Title: lp.GetTitle(), - DefaultMode: string(lp.GetDefaultMode()), - Priority: lp.GetPriority(), - PhasesConfigured: lp.GetPhasesConfigured(), - MinCurrent: lp.GetMinCurrent(), - MaxCurrent: lp.GetMaxCurrent(), - SmartCostLimit: lp.GetSmartCostLimit(), - Thresholds: lp.GetThresholds(), - Soc: lp.GetSocConfig(), - PlanEnergy: planEnergy, - PlanTime: planTime, - PlanPrecondition: int64(planPrecondition.Seconds()), - LimitEnergy: lp.GetLimitEnergy(), - LimitSoc: lp.GetLimitSoc(), + Title: lp.GetTitle(), + DefaultMode: string(lp.GetDefaultMode()), + Priority: lp.GetPriority(), + PhasesConfigured: lp.GetPhasesConfigured(), + MinCurrent: lp.GetMinCurrent(), + MaxCurrent: lp.GetMaxCurrent(), + MinPhysicalCurrent: lp.GetMinPhysicalCurrent(), + MaxPhysicalCurrent: lp.GetMaxPhysicalCurrent(), + SmartCostLimit: lp.GetSmartCostLimit(), + Thresholds: lp.GetThresholds(), + Soc: lp.GetSocConfig(), + PlanEnergy: planEnergy, + PlanTime: planTime, + PlanPrecondition: int64(planPrecondition.Seconds()), + LimitEnergy: lp.GetLimitEnergy(), + LimitSoc: lp.GetLimitSoc(), } } diff --git a/tests/basics.spec.ts b/tests/basics.spec.ts index 98a8fc152..c477365ac 100644 --- a/tests/basics.spec.ts +++ b/tests/basics.spec.ts @@ -80,6 +80,21 @@ test.describe("loadpoint settings", async () => { await expect(page.getByText("~ 3.7 kW")).toBeVisible(); await expect(page.getByText("~ 1.4 kW")).toBeVisible(); }); + + test("min/max current", async ({ page }) => { + await page.getByTestId("loadpoint-settings-button").last().click(); + const max = page.getByLabel("Max. current"); + const min = page.getByLabel("Min. current"); + await expect(max).toHaveValue("16"); + await expect(min).toHaveValue("6"); + + // default option range, no physical current limit + await expect(max.locator("option[value='100']")).toHaveCount(0); // not selectable by default + await expect(max.locator("option[value='64']")).toHaveCount(1); + await expect(max.locator("option[value='32']")).toHaveCount(1); + await expect(min.locator("option[value='5']")).toHaveCount(1); + await expect(min.locator("option[value='0.125']")).toHaveCount(1); + }); }); test.describe("network requests", async () => { diff --git a/tests/config-loadpoint.spec.ts b/tests/config-loadpoint.spec.ts index e92b078bf..e7fc21014 100644 --- a/tests/config-loadpoint.spec.ts +++ b/tests/config-loadpoint.spec.ts @@ -417,6 +417,91 @@ test.describe("loadpoint", async () => { ).toBeVisible(); }); + test("physical current", async ({ page }) => { + await start(CONFIG_EMPTY); + await page.goto("/#/config"); + await enableExperimental(page); + + /** + * 6A to 16A (default 11kW) + */ + + // add loadpoint, add charger + await newLoadpoint(page, "Carport"); + await addDemoCharger(page); + const lpModal = page.getByTestId("loadpoint-modal"); + await lpModal.getByRole("button", { name: "Save" }).click(); + await expectModalHidden(lpModal); + + // restart + await restart(CONFIG_EMPTY); + await page.reload(); + + // go do main ui (/) and open loadpoint settings modal + await page.goto("/"); + await page.getByTestId("loadpoint-settings-button").last().click(); + const settingsModal = page.getByTestId("loadpoint-settings-modal"); + await expectModalVisible(settingsModal); + const max = settingsModal.getByLabel("Max. current"); + const min = settingsModal.getByLabel("Min. current"); + await expect(max).toHaveValue("16"); + await expect(min).toHaveValue("6"); + + // no out of range values + await expect(max.locator("option[value='32']")).toHaveCount(0); + await expect(min.locator("option[value='5']")).toHaveCount(0); + + // avoid overlap by reducing options + await expect(max.locator("option[value='7']")).toHaveCount(1); + await expect(min.locator("option[value='15']")).toHaveCount(1); + await max.selectOption("14 A"); + await min.selectOption("8 A"); + await expect(max.locator("option[value='7']")).toHaveCount(0); + await expect(min.locator("option[value='15']")).toHaveCount(0); + + await settingsModal.getByRole("button", { name: "Close" }).click(); + await expectModalHidden(settingsModal); + + await page.reload(); + + // check persistance + await page.getByTestId("loadpoint-settings-button").last().click(); + await expectModalVisible(settingsModal); + await expect(max).toHaveValue("14"); + await expect(min).toHaveValue("8"); + + /** + * range 0.5A to 10A (e.g. heating) + */ + + await page.goto("/#/config"); + await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click(); + await expectModalVisible(lpModal); + await lpModal.getByTestId("chargerPower-other").click(); + await lpModal.getByLabel("Minimum current").fill("0.5"); + await lpModal.getByLabel("Maximum current").fill("10"); + await lpModal.getByRole("button", { name: "Save" }).click(); + await expectModalHidden(lpModal); + + // restart + await restart(CONFIG_EMPTY); + await page.reload(); + + // check loadpoint settings + await page.goto("/"); + await page.getByTestId("loadpoint-settings-button").last().click(); + await expectModalVisible(settingsModal); + await expect(max).toHaveValue("10"); // adjusted max + await expect(min).toHaveValue("8"); // kept min + + // check range + await expect(max.locator("option[value='11']")).toHaveCount(0); + await expect(max.locator("option[value='10']")).toHaveCount(1); + await expect(min.locator("option[value='1']")).toHaveCount(1); + await expect(min.locator("option[value='0.5']")).toHaveCount(1); + await expect(min.locator("option[value='0.25']")).toHaveCount(0); + }); + test("user-defined charger", async ({ page }) => { await start(CONFIG_EMPTY); await page.goto("/#/config");