Persist phases/min/max current (BC) (#11571)

This commit is contained in:
andig 2024-01-27 11:25:27 +01:00 • committed by GitHub
parent 91592bbd26
commit 648fe0cfdf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 216 additions and 271 deletions

View file

@ -12,19 +12,11 @@
{{ loadpointTitle }}
</div>
</h3>
<LoadpointSettingsButton
v-if="settingsButtonVisible"
:id="id"
class="d-block d-sm-none"
/>
<LoadpointSettingsButton :id="id" class="d-block d-sm-none" />
</div>
<div class="mb-3 d-flex align-items-center">
<Mode class="flex-grow-1" :mode="mode" @updated="setTargetMode" />
<LoadpointSettingsButton
v-if="settingsButtonVisible"
:id="id"
class="d-none d-sm-block ms-2"
/>
<LoadpointSettingsButton :id="id" class="d-none d-sm-block ms-2" />
</div>
</div>
<LoadpointSettingsModal
@ -178,6 +170,7 @@ export default {
phases: Number,
phasesConfigured: Number,
phasesActive: Number,
phases1p3p: Boolean,
minCurrent: Number,
maxCurrent: Number,
chargeCurrent: Number,
@ -232,9 +225,6 @@ export default {
settingsModal: function () {
return this.collectProps(LoadpointSettingsModal);
},
settingsButtonVisible: function () {
return this.$hiddenFeatures() || [0, 1, 3].includes(this.phasesConfigured);
},
vehicleProps: function () {
return this.collectProps(Vehicle);
},

View file

@ -2,6 +2,7 @@
<button
type="button"
class="btn btn-sm btn-outline-secondary position-relative border-0 p-2 evcc-gray"
data-testid="loadpoint-settings-button"
@click="openModal"
>
<shopicon-regular-adjust size="s"></shopicon-regular-adjust>

View file

@ -23,14 +23,10 @@
</div>
<div class="modal-body">
<div class="container">
<h4
v-if="showConfigurablePhases || showCurrentSettings"
class="d-flex align-items-center mb-3 mt-4 text-evcc"
>
<h4 class="d-flex align-items-center mb-3 mt-4 text-evcc">
{{ $t("main.loadpointSettings.currents") }}
<shopicon-bold-lightning class="ms-1"></shopicon-bold-lightning>
</h4>
<div v-if="showConfigurablePhases" class="mb-3 row">
<div class="mb-3 row">
<label
:for="formId('phases_0')"
class="col-sm-4 col-form-label pt-0"
@ -38,71 +34,37 @@
{{ $t("main.loadpointSettings.phasesConfigured.label") }}
</label>
<div class="col-sm-8 pe-0">
<div class="form-check">
<div
v-for="phases in phasesOptions"
:key="phases"
class="form-check"
>
<input
:id="formId('phases_0')"
:id="formId(`phases_${phases}`)"
v-model.number="selectedPhases"
class="form-check-input"
type="radio"
:name="formId('phases')"
:value="0"
:value="phases"
@change="changePhasesConfigured"
/>
<label class="form-check-label" :for="formId('phases_0')">
<label
class="form-check-label"
:for="formId(`phases_${phases}`)"
>
{{
$t(
"main.loadpointSettings.phasesConfigured.phases_0"
`main.loadpointSettings.phasesConfigured.phases_${phases}`
)
}}
</label>
</div>
<div class="form-check">
<input
:id="formId('phases_1')"
v-model.number="selectedPhases"
class="form-check-input"
type="radio"
:name="formId('phases')"
:value="1"
@change="changePhasesConfigured"
/>
<label class="form-check-label" :for="formId('phases_1')">
{{
$t(
"main.loadpointSettings.phasesConfigured.phases_1"
)
}}
<small>
<small v-if="phases > 0">
{{
$t(
"main.loadpointSettings.phasesConfigured.phases_1_hint",
{ min: minPower1p, max: maxPower1p }
)
}}
</small>
</label>
</div>
<div class="form-check">
<input
:id="formId('phases_3')"
v-model.number="selectedPhases"
class="form-check-input"
type="radio"
:name="formId('phases')"
:value="3"
@change="changePhasesConfigured"
/>
<label class="form-check-label" :for="formId('phases_3')">
{{
$t(
"main.loadpointSettings.phasesConfigured.phases_3"
)
}}
<small>
{{
$t(
"main.loadpointSettings.phasesConfigured.phases_3_hint",
{ min: minPower3p, max: maxPower3p }
`main.loadpointSettings.phasesConfigured.phases_${phases}_hint`,
{
min: minPowerPhases(phases),
max: maxPowerPhases(phases),
}
)
}}
</small>
@ -111,7 +73,7 @@
</div>
</div>
<div v-if="$hiddenFeatures()" class="mb-3 row">
<div class="mb-3 row">
<label
:for="formId('maxcurrent')"
class="col-sm-4 col-form-label pt-0 pt-sm-2"
@ -137,7 +99,7 @@
</div>
</div>
<div v-if="$hiddenFeatures()" class="mb-3 row">
<div class="mb-3 row">
<label
:for="formId('mincurrent')"
class="col-sm-4 col-form-label pt-0 pt-sm-2"
@ -163,12 +125,6 @@
</div>
</div>
</div>
<p class="small mt-3 text-muted mb-0">
<strong class="text-evcc">
{{ $t("main.loadpointSettings.disclaimerHint") }}
</strong>
{{ $t("main.loadpointSettings.disclaimerText") }}
</p>
</div>
</div>
</div>
@ -177,8 +133,6 @@
</template>
<script>
import "@h2d2/shopicons/es/bold/lightning";
import "@h2d2/shopicons/es/bold/car3";
import formatter from "../mixins/formatter";
const V = 230;
@ -203,6 +157,7 @@ export default {
id: [String, Number],
phasesConfigured: Number,
phasesActive: Number,
phases1p3p: Boolean,
minSoc: Number,
maxCurrent: Number,
minCurrent: Number,
@ -217,47 +172,29 @@ export default {
};
},
computed: {
maxPower1p: function () {
return this.fmtKw(this.maxCurrent * V);
},
minPower1p: function () {
return this.fmtKw(this.minCurrent * V);
},
maxPower3p: function () {
return this.fmtKw(this.maxCurrent * V * 3);
},
minPower3p: function () {
return this.fmtKw(this.minCurrent * V * 3);
phasesOptions: function () {
if (this.phases1p3p) {
return [PHASES_AUTO, PHASES_3, PHASES_1];
}
return [PHASES_3, PHASES_1];
},
maxPower: function () {
switch (this.phasesConfigured) {
case PHASES_AUTO:
return this.maxPower3p;
case PHASES_3:
return this.maxPower3p;
case PHASES_1:
return this.maxPower1p;
default:
return this.fmtKw(this.maxCurrent * V * this.phasesActive);
if (this.phasesConfigured === PHASES_AUTO) {
return this.maxPowerPhases(3);
}
if ([PHASES_3, PHASES_1].includes(this.phasesConfigured)) {
return this.maxPowerPhases(this.phasesConfigured);
}
return this.fmtKw(this.maxCurrent * V * this.phasesActive);
},
minPower: function () {
switch (this.phasesConfigured) {
case PHASES_AUTO:
return this.minPower1p;
case PHASES_3:
return this.minPower3p;
case PHASES_1:
return this.minPower1p;
default:
return this.fmtKw(this.minCurrent * V * this.phasesActive);
if (this.phasesConfigured === PHASES_AUTO) {
return this.minPowerPhases(1);
}
},
showConfigurablePhases: function () {
return [PHASES_AUTO, PHASES_3, PHASES_1].includes(this.phasesConfigured);
},
showCurrentSettings: function () {
return this.$hiddenFeatures();
if ([PHASES_3, PHASES_1].includes(this.phasesConfigured)) {
return this.minPowerPhases(this.phasesConfigured);
}
return this.fmtKw(this.minCurrent * V * this.phasesActive);
},
minCurrentOptions: function () {
const opt1 = [...range(Math.floor(this.maxCurrent), 1), 0.5, 0.25, 0.125];
@ -287,6 +224,12 @@ export default {
},
},
methods: {
maxPowerPhases: function (phases) {
return this.fmtKw(this.maxCurrent * V * phases);
},
minPowerPhases: function (phases) {
return this.fmtKw(this.minCurrent * V * phases);
},
formId: function (name) {
return `loadpoint_${this.id}_${name}`;
},

View file

@ -236,7 +236,6 @@ loadpoints:
- title: Carport
charger: charger_1
mode: pv
phases: 1
meter: meter_charger_1
vehicle: vehicle_1
- title: Garage

View file

@ -14,6 +14,7 @@ const (
DisableThreshold = "disableThreshold"
PhasesConfigured = "phasesConfigured" // configured phases (1/3, 0 for auto on 1p3p chargers, nil for plain chargers)
Phases1p3p = "phases1p3p" // phase switcher (1p3p chargers)
PhasesEnabled = "phasesEnabled" // enabled phases (1/3)
PhasesActive = "phasesActive" // active phases as used by vehicle (1/2/3)

View file

@ -108,21 +108,25 @@ type Loadpoint struct {
vmu sync.RWMutex // guard vehicle
Mode_ api.ChargeMode `mapstructure:"mode"` // Default charge mode, used for disconnect
Title_ string `mapstructure:"title"` // UI title
Priority_ int `mapstructure:"priority"` // Priority
ConfiguredPhases int `mapstructure:"phases"` // Charger configured phase mode 0/1/3
ChargerRef string `mapstructure:"charger"` // Charger reference
VehicleRef string `mapstructure:"vehicle"` // Vehicle reference
MeterRef string `mapstructure:"meter"` // Charge meter reference
Soc SocConfig
Enable, Disable ThresholdConfig
Title_ string `mapstructure:"title"` // UI title
Priority_ int `mapstructure:"priority"` // Priority
ChargerRef string `mapstructure:"charger"` // Charger reference
VehicleRef string `mapstructure:"vehicle"` // Vehicle reference
MeterRef string `mapstructure:"meter"` // Charge meter reference
Soc SocConfig
Enable, Disable ThresholdConfig
GuardDuration time.Duration // charger enable/disable minimum holding time
MinCurrent float64 // PV mode: start current Min+PV mode: min current
MaxCurrent float64 // Max allowed current. Physically ensured by the charger
GuardDuration time.Duration // charger enable/disable minimum holding time
// TODO deprecated
ConfiguredPhases_ int `mapstructure:"phases"`
MinCurrent_ float64 `mapstructure:"minCurrent"`
MaxCurrent_ float64 `mapstructure:"maxCurrent"`
limitSoc int // Session limit for soc
limitEnergy float64 // Session limit for energy
minCurrent float64 // PV mode: start current Min+PV mode: min current
maxCurrent float64 // Max allowed current. Physically ensured by the charger
configuredPhases int // Charger configured phase mode 0/1/3
limitSoc int // Session limit for soc
limitEnergy float64 // Session limit for energy
mode api.ChargeMode
enabled bool // Charger enabled state
@ -200,14 +204,6 @@ func NewLoadpointFromConfig(log *util.Logger, settings *Settings, other map[stri
lp.Soc.Poll.Mode = pollCharging
}
if lp.MinCurrent == 0 {
lp.log.WARN.Println("minCurrent must not be zero")
}
if lp.MaxCurrent < lp.MinCurrent {
lp.log.WARN.Println("maxCurrent must be larger than minCurrent")
}
if lp.MeterRef != "" {
dev, err := config.Meters().ByName(lp.MeterRef)
if err != nil {
@ -235,17 +231,29 @@ func NewLoadpointFromConfig(log *util.Logger, settings *Settings, other map[stri
lp.charger = dev.Instance()
lp.configureChargerType(lp.charger)
// setup fixed phases:
// - simple charger starts with phases config if specified or 3p
// - switchable charger starts at 0p since we don't know the current setting
if _, ok := lp.charger.(api.PhaseSwitcher); !ok {
if lp.ConfiguredPhases == 0 {
lp.ConfiguredPhases = 3
lp.log.WARN.Println("phases not configured, assuming 3p")
// phase switching defaults based on charger capabilities
if !lp.hasPhaseSwitching() {
lp.configuredPhases = 3
}
// TODO deprecated
if lp.MinCurrent_ > 0 {
lp.log.WARN.Println("deprecated: minCurrent setting is ignored, please remove")
if _, err := lp.settings.Float(keys.MinCurrent); err != nil {
lp.settings.SetFloat(keys.MinCurrent, lp.MinCurrent_)
}
}
if lp.MaxCurrent_ > 0 {
lp.log.WARN.Println("deprecated: maxcurrent setting is ignored, please remove")
if _, err := lp.settings.Float(keys.MaxCurrent); err != nil {
lp.settings.SetFloat(keys.MaxCurrent, lp.MaxCurrent_)
}
}
if lp.ConfiguredPhases_ > 0 {
lp.log.WARN.Println("deprecated: phases setting is ignored, please remove")
if _, err := lp.settings.Int(keys.PhasesConfigured); err != nil {
lp.settings.SetInt(keys.PhasesConfigured, int64(lp.ConfiguredPhases_))
}
lp.phases = lp.ConfiguredPhases
} else if lp.ConfiguredPhases != 0 {
lp.log.WARN.Printf("locking phase config to %dp for switchable charger", lp.ConfiguredPhases)
}
// validate thresholds
@ -275,8 +283,8 @@ func NewLoadpoint(log *util.Logger, settings *Settings) *Loadpoint {
bus: bus, // event bus
mode: api.ModeOff,
status: api.StatusNone,
MinCurrent: 6, // A
MaxCurrent: 16, // A
minCurrent: 6, // A
maxCurrent: 16, // A
Soc: SocConfig{
Poll: PollConfig{
Interval: pollInterval,
@ -300,6 +308,15 @@ func (lp *Loadpoint) restoreSettings() {
if v, err := lp.settings.String(keys.Mode); err == nil && v != "" {
lp.setMode(api.ChargeMode(v))
}
if v, err := lp.settings.Int(keys.PhasesConfigured); err == nil && (v > 0 || lp.hasPhaseSwitching()) {
lp.setConfiguredPhases(int(v))
}
if v, err := lp.settings.Float(keys.MinCurrent); err == nil && v > 0 {
lp.setMinCurrent(v)
}
if v, err := lp.settings.Float(keys.MaxCurrent); err == nil && v > 0 {
lp.setMaxCurrent(v)
}
if v, err := lp.settings.Int(keys.LimitSoc); err == nil && v > 0 {
lp.setLimitSoc(int(v))
}
@ -571,7 +588,8 @@ func (lp *Loadpoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
lp.publish(keys.EnableThreshold, lp.Enable.Threshold)
lp.publish(keys.DisableThreshold, lp.Disable.Threshold)
lp.setConfiguredPhases(lp.ConfiguredPhases)
lp.publish(keys.PhasesConfigured, lp.configuredPhases)
lp.publish(keys.Phases1p3p, lp.hasPhaseSwitching())
lp.publish(keys.PhasesEnabled, lp.phases)
lp.publish(keys.PhasesActive, lp.ActivePhases())
lp.publishTimer(phaseTimer, 0, timerInactive)
@ -970,17 +988,16 @@ func (lp *Loadpoint) resetPhaseTimer() {
// scalePhasesRequired validates if fixed phase configuration matches enabled phases
func (lp *Loadpoint) scalePhasesRequired() bool {
_, ok := lp.charger.(api.PhaseSwitcher)
return ok && lp.ConfiguredPhases != 0 && lp.ConfiguredPhases != lp.GetPhases()
return lp.hasPhaseSwitching() && lp.configuredPhases != 0 && lp.configuredPhases != lp.GetPhases()
}
// scalePhasesIfAvailable scales if api.PhaseSwitcher is available
func (lp *Loadpoint) scalePhasesIfAvailable(phases int) error {
if lp.ConfiguredPhases != 0 {
phases = lp.ConfiguredPhases
if lp.configuredPhases != 0 {
phases = lp.configuredPhases
}
if _, ok := lp.charger.(api.PhaseSwitcher); ok {
if lp.hasPhaseSwitching() {
return lp.scalePhases(phases)
}
@ -1041,7 +1058,7 @@ func (lp *Loadpoint) pvScalePhases(sitePower, minCurrent, maxCurrent float64) bo
var waiting bool
activePhases := lp.ActivePhases()
availablePower := lp.chargePower - sitePower
scalable := (sitePower > 0 || !lp.enabled) && activePhases > 1 && lp.ConfiguredPhases < 3
scalable := (sitePower > 0 || !lp.enabled) && activePhases > 1 && lp.configuredPhases < 3
// scale down phases
if targetCurrent := powerToCurrent(availablePower, activePhases); targetCurrent < minCurrent && scalable {
@ -1137,7 +1154,7 @@ func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batter
maxCurrent := lp.effectiveMaxCurrent()
// switch phases up/down
if _, ok := lp.charger.(api.PhaseSwitcher); ok {
if lp.hasPhaseSwitching() {
_ = lp.pvScalePhases(sitePower, minCurrent, maxCurrent)
}
@ -1294,7 +1311,7 @@ func (lp *Loadpoint) updateChargeCurrents() {
// updateChargeVoltages uses PhaseVoltages interface to count phases with nominal grid voltage
func (lp *Loadpoint) updateChargeVoltages() {
if _, ok := lp.charger.(api.PhaseSwitcher); ok {
if lp.hasPhaseSwitching() {
return // we don't need the voltages
}
@ -1563,7 +1580,7 @@ func (lp *Loadpoint) Update(sitePower float64, autoCharge, batteryBuffered, batt
err = lp.setLimit(0, false)
case lp.scalePhasesRequired():
err = lp.scalePhases(lp.ConfiguredPhases)
err = lp.scalePhases(lp.configuredPhases)
case lp.remoteControlled(loadpoint.RemoteHardDisable):
remoteDisabled = loadpoint.RemoteHardDisable

View file

@ -35,11 +35,11 @@ type API interface {
// GetMinCurrent returns the min charging current
GetMinCurrent() float64
// SetMinCurrent sets the min charging current
SetMinCurrent(float64)
SetMinCurrent(float64) error
// GetMaxCurrent returns the max charging current
GetMaxCurrent() float64
// SetMaxCurrent sets the max charging current
SetMaxCurrent(float64)
SetMaxCurrent(float64) error
// GetMode returns the charge mode
GetMode() api.ChargeMode

View file

@ -480,9 +480,11 @@ func (mr *MockAPIMockRecorder) SetLimitSoc(arg0 any) *gomock.Call {
}
// SetMaxCurrent mocks base method.
func (m *MockAPI) SetMaxCurrent(arg0 float64) {
func (m *MockAPI) SetMaxCurrent(arg0 float64) error {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetMaxCurrent", arg0)
ret := m.ctrl.Call(m, "SetMaxCurrent", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// SetMaxCurrent indicates an expected call of SetMaxCurrent.
@ -492,9 +494,11 @@ func (mr *MockAPIMockRecorder) SetMaxCurrent(arg0 any) *gomock.Call {
}
// SetMinCurrent mocks base method.
func (m *MockAPI) SetMinCurrent(arg0 float64) {
func (m *MockAPI) SetMinCurrent(arg0 float64) error {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetMinCurrent", arg0)
ret := m.ctrl.Call(m, "SetMinCurrent", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// SetMinCurrent indicates an expected call of SetMinCurrent.

View file

@ -108,7 +108,7 @@ func (lp *Loadpoint) GetPhases() int {
// SetPhases sets loadpoint enabled phases
func (lp *Loadpoint) SetPhases(phases int) error {
// limit auto mode (phases=0) to scalable charger
if _, ok := lp.charger.(api.PhaseSwitcher); !ok && phases == 0 {
if !lp.hasPhaseSwitching() && phases == 0 {
return fmt.Errorf("invalid number of phases: %d", phases)
}
@ -118,10 +118,13 @@ func (lp *Loadpoint) SetPhases(phases int) error {
// set new default
lp.log.DEBUG.Println("set phases:", phases)
lp.Lock()
lp.setConfiguredPhases(phases)
lp.Unlock()
// apply immediately if not 1p3p
if _, ok := lp.charger.(api.PhaseSwitcher); !ok {
if !lp.hasPhaseSwitching() {
lp.setPhases(phases)
}
@ -324,40 +327,62 @@ func (lp *Loadpoint) GetChargePowerFlexibility() float64 {
func (lp *Loadpoint) GetMinCurrent() float64 {
lp.RLock()
defer lp.RUnlock()
return lp.MinCurrent
return lp.minCurrent
}
// setMinCurrent sets the min loadpoint current (no mutex)
func (lp *Loadpoint) setMinCurrent(current float64) {
lp.minCurrent = current
lp.publish(keys.MinCurrent, lp.minCurrent)
lp.settings.SetFloat(keys.MinCurrent, lp.minCurrent)
}
// SetMinCurrent sets the min loadpoint current
func (lp *Loadpoint) SetMinCurrent(current float64) {
func (lp *Loadpoint) SetMinCurrent(current float64) error {
lp.Lock()
defer lp.Unlock()
lp.log.DEBUG.Println("set min current:", current)
if current != lp.MinCurrent {
lp.MinCurrent = current
lp.publish(keys.MinCurrent, lp.MinCurrent)
if current > lp.maxCurrent {
return errors.New("min current must be smaller or equal than max current")
}
lp.log.DEBUG.Println("set min current:", current)
if current != lp.minCurrent {
lp.setMinCurrent(current)
}
return nil
}
// GetMaxCurrent returns the max loadpoint current
func (lp *Loadpoint) GetMaxCurrent() float64 {
lp.RLock()
defer lp.RUnlock()
return lp.MaxCurrent
return lp.maxCurrent
}
// setMaxCurrent sets the max loadpoint current
func (lp *Loadpoint) setMaxCurrent(current float64) {
lp.maxCurrent = current
lp.publish(keys.MaxCurrent, lp.maxCurrent)
lp.settings.SetFloat(keys.MaxCurrent, lp.maxCurrent)
}
// SetMaxCurrent sets the max loadpoint current
func (lp *Loadpoint) SetMaxCurrent(current float64) {
func (lp *Loadpoint) SetMaxCurrent(current float64) error {
lp.Lock()
defer lp.Unlock()
lp.log.DEBUG.Println("set max current:", current)
if current != lp.MaxCurrent {
lp.MaxCurrent = current
lp.publish(keys.MaxCurrent, lp.MaxCurrent)
if current < lp.minCurrent {
return errors.New("max current must be greater or equal than min current")
}
lp.log.DEBUG.Println("set max current:", current)
if current != lp.maxCurrent {
lp.setMaxCurrent(current)
}
return nil
}
// GetMinPower returns the min loadpoint power for a single phase

View file

@ -7,17 +7,9 @@ import (
// setConfiguredPhases sets the default phase configuration
func (lp *Loadpoint) setConfiguredPhases(phases int) {
lp.Lock()
defer lp.Unlock()
lp.ConfiguredPhases = phases
// publish 1p3p capability and phase configuration
if _, ok := lp.charger.(api.PhaseSwitcher); ok {
lp.publish(keys.PhasesConfigured, lp.ConfiguredPhases)
} else {
lp.publish(keys.PhasesConfigured, nil)
}
lp.configuredPhases = phases
lp.publish(keys.PhasesConfigured, lp.configuredPhases)
lp.settings.SetInt(keys.PhasesConfigured, int64(lp.configuredPhases))
}
// setPhases sets the number of enabled phases without modifying the charger
@ -93,8 +85,8 @@ func (lp *Loadpoint) maxActivePhases() int {
}
// if 1p3p supported then assume configured limit or 3p
if _, ok := lp.charger.(api.PhaseSwitcher); ok {
physical = lp.ConfiguredPhases
if lp.hasPhaseSwitching() {
physical = lp.configuredPhases
}
return min(expect(vehicle), expect(physical), expect(measured))
@ -107,3 +99,8 @@ func (lp *Loadpoint) getVehiclePhases() int {
return 0
}
func (lp *Loadpoint) hasPhaseSwitching() bool {
_, ok := lp.charger.(api.PhaseSwitcher)
return ok
}

View file

@ -87,7 +87,7 @@ func TestMaxActivePhases(t *testing.T) {
vehicle.EXPECT().Phases().Return(tc.vehicle).MinTimes(1)
lp := &Loadpoint{
ConfiguredPhases: dflt, // fixed phases or default
configuredPhases: dflt, // fixed phases or default
vehicle: vehicle,
phases: tc.physical,
measuredPhases: tc.measuredPhases,
@ -187,10 +187,10 @@ func TestPvScalePhases(t *testing.T) {
progress: NewProgress(0, 10), // silence nil panics
wakeUpTimer: NewTimer(), // silence nil panics
mode: api.ModeNow,
MinCurrent: minA,
MaxCurrent: maxA,
minCurrent: minA,
maxCurrent: maxA,
vehicle: vehicle,
ConfiguredPhases: 0, // allow switching
configuredPhases: 0, // allow switching
phases: tc.physical,
status: api.StatusC,
}
@ -366,8 +366,8 @@ func TestPvScalePhasesTimer(t *testing.T) {
log: util.NewLogger("foo"),
clock: clock,
charger: charger,
MinCurrent: minA,
MaxCurrent: maxA,
minCurrent: minA,
maxCurrent: maxA,
phases: tc.phases,
measuredPhases: tc.measuredPhases,
status: api.StatusC,
@ -431,8 +431,8 @@ func TestScalePhasesIfAvailable(t *testing.T) {
plainCharger,
phaseCharger,
},
MinCurrent: minA,
ConfiguredPhases: tc.dflt, // fixed phases or default
minCurrent: minA,
configuredPhases: tc.dflt, // fixed phases or default
phases: tc.physical, // current phase status
}

View file

@ -75,7 +75,7 @@ func attachListeners(t *testing.T, lp *Loadpoint) {
if charger, ok := lp.charger.(*api.MockCharger); ok && charger != nil {
charger.EXPECT().Enabled().Return(true, nil)
charger.EXPECT().MaxCurrent(int64(lp.MinCurrent)).Return(nil)
charger.EXPECT().MaxCurrent(int64(lp.minCurrent)).Return(nil)
}
uiChan, pushChan, lpChan := createChannels(t)
@ -88,8 +88,8 @@ func TestNew(t *testing.T) {
if lp.phases != 0 {
t.Errorf("Phases %v", lp.phases)
}
if lp.MaxCurrent != maxA {
t.Errorf("MaxCurrent %v", lp.MaxCurrent)
if lp.maxCurrent != maxA {
t.Errorf("MaxCurrent %v", lp.maxCurrent)
}
if lp.status != api.StatusNone {
t.Errorf("status %v", lp.status)
@ -164,8 +164,8 @@ func TestUpdatePowerZero(t *testing.T) {
chargeTimer: &Null{}, // silence nil panics
wakeUpTimer: NewTimer(),
sessionEnergy: NewEnergyMetrics(),
MinCurrent: minA,
MaxCurrent: maxA,
minCurrent: minA,
maxCurrent: maxA,
phases: 1,
status: tc.status, // no status change
}
@ -311,8 +311,8 @@ func TestPVHysteresis(t *testing.T) {
log: util.NewLogger("foo"),
clock: clck,
charger: charger,
MinCurrent: minA,
MaxCurrent: maxA,
minCurrent: minA,
maxCurrent: maxA,
phases: phases,
measuredPhases: phases,
Enable: ThresholdConfig{
@ -359,8 +359,8 @@ func TestPVHysteresisForStatusOtherThanC(t *testing.T) {
lp := &Loadpoint{
log: util.NewLogger("foo"),
clock: clck,
MinCurrent: minA,
MaxCurrent: maxA,
minCurrent: minA,
maxCurrent: maxA,
phases: phases,
measuredPhases: phases,
}
@ -401,8 +401,8 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) {
progress: NewProgress(0, 10), // silence nil panics
wakeUpTimer: NewTimer(), // silence nil panics
// coordinator: coordinator.NewDummy(), // silence nil panics
MinCurrent: minA,
MaxCurrent: maxA,
minCurrent: minA,
maxCurrent: maxA,
vehicle: vehicle, // needed for targetSoc check
socEstimator: socEstimator, // instead of vehicle: vehicle,
mode: api.ModeNow,
@ -479,8 +479,8 @@ func TestSetModeAndSocAtDisconnect(t *testing.T) {
chargeTimer: &Null{}, // silence nil panics
wakeUpTimer: NewTimer(),
sessionEnergy: NewEnergyMetrics(),
MinCurrent: minA,
MaxCurrent: maxA,
minCurrent: minA,
maxCurrent: maxA,
status: api.StatusC,
Mode_: api.ModeOff, // default mode
}
@ -550,8 +550,8 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
chargeTimer: &Null{}, // silence nil panics
wakeUpTimer: NewTimer(),
sessionEnergy: NewEnergyMetrics(),
MinCurrent: minA,
MaxCurrent: maxA,
minCurrent: minA,
maxCurrent: maxA,
status: api.StatusC,
}

View file

@ -47,8 +47,8 @@ func TestPublishSocAndRange(t *testing.T) {
chargeTimer: &Null{}, // silence nil panics
socEstimator: soc.NewEstimator(log, charger, vehicle, false),
sessionEnergy: NewEnergyMetrics(),
MinCurrent: minA,
MaxCurrent: maxA,
minCurrent: minA,
maxCurrent: maxA,
phases: 1,
mode: api.ModeNow,
}
@ -252,8 +252,8 @@ func TestReconnectVehicle(t *testing.T) {
chargeTimer: &Null{}, // silence nil panics
wakeUpTimer: NewTimer(),
sessionEnergy: NewEnergyMetrics(),
MinCurrent: minA,
MaxCurrent: maxA,
minCurrent: minA,
maxCurrent: maxA,
phases: 1,
mode: api.ModeNow,
}

View file

@ -116,9 +116,6 @@ loadpoints:
charger: wallbe # charger
meter: charge # charge meter
mode: "off" # default charge mode to apply when vehicle is disconnected; use "off" to disable by default if charger is publicly available
phases: 3 # electrical connection (normal charger: default 3 for 3 phase, 1p3p charger: 0 for "auto" or 1/3 for fixed phases)
minCurrent: 6 # minimum charge current (default 6A)
maxCurrent: 16 # maximum charge current (default 16A)
# remaining settings are experts-only and best left at default values
priority: 0 # relative priority for concurrent charging in PV mode with multiple loadpoints (higher values have higher priority)

View file

@ -154,8 +154,6 @@ solar = "Solar"
[main.loadpointSettings]
currents = "Corrent de càrrega"
default = "defecte"
disclaimerHint = "Nota:"
disclaimerText = "Els canvis encara no són persistents i es restableixen al iniciar el servei."
title = "Configuracions {0}"
vehicle = "Vehicle"

View file

@ -154,8 +154,6 @@ solar = "Slunce"
[main.loadpointSettings]
currents = "Nabíjecí proud"
default = "výchozí"
disclaimerHint = "Poznámka:"
disclaimerText = "Po restartovaní serveru budou zmeny ztraceny"
title = "Nastavení {0}"
vehicle = "Vozidlo"

View file

@ -211,8 +211,6 @@ solar = "solenergi"
[main.loadpointSettings]
currents = "Ladestrøm"
default = "standard"
disclaimerHint = "Bemærk:"
disclaimerText = "Ændringer vil gå tabt, efter at serveren er genstartet."
onlyForSocBasedCharging = "Disse indstillinger er kun tilgængelig for køretøjer med kendt opladningsniveau"
title = "Indstillinger {0}"
vehicle = "Køretøj"

View file

@ -213,8 +213,6 @@ solar = "Sonne"
[main.loadpointSettings]
currents = "Ladestrom"
default = "default"
disclaimerHint = "Hinweis:"
disclaimerText = "Änderungen gehen nach einem Neustart des Servers verloren."
onlyForSocBasedCharging = "Diese Optionen sind nur für Fahrzeuge mit bekanntem Ladestand verfügbar."
title = "Einstellungen {0}"
vehicle = "Fahrzeug"

View file

@ -210,8 +210,6 @@ solar = "Solar"
[main.loadpointSettings]
currents = "Charging current"
default = "default"
disclaimerHint = "Note:"
disclaimerText = "Changes will be lost after the server is restarted."
onlyForSocBasedCharging = "These options are only available for vehicles with known charging level."
title = "Settings {0}"
vehicle = "Vehicle"

View file

@ -222,8 +222,6 @@ solar = "Solar"
[main.loadpointSettings]
currents = "Corriente de carga"
default = "defecto"
disclaimerHint = "Nota:"
disclaimerText = "Actualmente, los cambios aún no son persistentes y se restablecen al iniciar el servicio."
onlyForSocBasedCharging = "Estas opciones sólo están disponibles para vehículos con nivel de carga conocido"
title = "Configuraciones {0}"
vehicle = "Vehículo"

View file

@ -199,8 +199,6 @@ solar = "Aurinkoenergia"
[main.loadpointSettings]
currents = "Latausvirta"
default = "oletus"
disclaimerHint = "Huomautus:"
disclaimerText = "Muutokset eivät ole voimassa serverin uudelleen käynnistyksen jälkeen."
title = "Asetukset {0}"
vehicle = "Ajoneuvo"

View file

@ -216,8 +216,6 @@ solar = "Solaire"
[main.loadpointSettings]
currents = "Courant de charge"
default = "défaut"
disclaimerHint = "Remarque:"
disclaimerText = "Les modifications seront perdues après le redémarrage du serveur."
onlyForSocBasedCharging = "Ces options sont disponibles uniquement pour les véhicules avec niveau de charge connu."
title = "Réglages {0}"
vehicle = "Véhicule"

View file

@ -218,8 +218,6 @@ solar = "Solarno"
[main.loadpointSettings]
currents = "Struja napajanja"
default = "zadano"
disclaimerHint = "Bilješka:"
disclaimerText = "Promjene će se izgubiti nakon ponovnog pokretanja poslužitelja."
onlyForSocBasedCharging = "Ove su opcije dostupne samo za vozila s poznatom razinom napunjenosti."
title = "Postavke {0}"
vehicle = "Vozilo"

View file

@ -101,8 +101,6 @@ remoteDisabledSoft = "{source}: Ricarica FV adattiva disabilitata"
[main.loadpointSettings]
currents = "Corrente di carica"
default = "default"
disclaimerHint = "Nota:"
disclaimerText = "Le modifiche andranno perse dopo il riavvio del server."
title = "Impostazioni {0}"
vehicle = "Veicolo"

View file

@ -94,8 +94,6 @@ power = "Leeschtung"
[main.loadpointSettings]
currents = "Ladestroum"
default = "Standard"
disclaimerHint = "Notiz:"
disclaimerText = "Ännerunge ginn verluer nodeems de Server nei gestart ass."
title = "Astellungen {0}"
vehicle = "Gefier"

View file

@ -216,8 +216,6 @@ solar = "Saulės"
[main.loadpointSettings]
currents = "Įkrovimo srovė"
default = "standartiškai"
disclaimerHint = "Pastaba:"
disclaimerText = "Šie pakeitimai neišlieka po evcc serverio restarto."
onlyForSocBasedCharging = "Šie pasirinkimai galimi tik automobiliams su žinomu įkrovos lygiu."
title = "Nustatymai {0}"
vehicle = "Automobilis"

View file

@ -195,8 +195,6 @@ solar = "Zon"
[main.loadpointSettings]
currents = "Laadstroom"
default = "standaard"
disclaimerHint = "Let op:"
disclaimerText = "Wijzigingen gaan verloren als de server herstart."
title = "Instellingen {0}"
vehicle = "Voertuig"

View file

@ -192,8 +192,6 @@ solar = "Solenergi"
[main.loadpointSettings]
currents = "Ladestrøm"
default = "forvalg"
disclaimerHint = "Merknad:"
disclaimerText = "Endringer vil gå tapt ved omstart av serveren"
title = "Innstillinger for {0}"
vehicle = "Kjøretøy"

View file

@ -217,8 +217,6 @@ solar = "Słoneczna"
[main.loadpointSettings]
currents = "Prąd ładowania"
default = "domyślny"
disclaimerHint = "Uwaga:"
disclaimerText = "Zmiany zostaną zresetowane po restarcie serwera."
onlyForSocBasedCharging = "Opcje te są dostępne tylko dla pojazdów ze znanym poziomem naładowania."
title = "Ustawienia {0}"
vehicle = "Pojazd"

View file

@ -216,8 +216,6 @@ solar = "Sol"
[main.loadpointSettings]
currents = "Corrente de carga"
default = "predefinado"
disclaimerHint = "Aviso:"
disclaimerText = "As alterações serão perdidas depois que o servidor for reiniciado."
onlyForSocBasedCharging = "Estas opções só estão disponíveis para veículos com nível de carregamento conhecido."
title = "Configurações {0}"
vehicle = "Veículo"

View file

@ -160,8 +160,6 @@ power = "Putere"
[main.loadpointSettings]
currents = "Curent încărcare"
default = "default"
disclaimerHint = "Nota:"
disclaimerText = "Modificările se vor pierde după restartul serverului."
title = "Setări {0}"
vehicle = "Vehicul"

View file

@ -123,8 +123,6 @@ power = "Мощность"
[main.loadpointSettings]
currents = "Ток зарядки"
default = "по умолчанию"
disclaimerHint = "Заметка:"
disclaimerText = "Изменения будут потеряны после перезагрузки сервера"
title = "Настройки {0}"
vehicle = "Транспортное средство"

View file

@ -211,8 +211,6 @@ solar = "Sončna energija"
[main.loadpointSettings]
currents = "Polnilni tok"
default = "privzeto"
disclaimerHint = "Opomba:"
disclaimerText = "Po ponovnem zagonu strežnika bodo spremembe izgubljene."
onlyForSocBasedCharging = "Te možnosti so na voljo samo za vozila z znano stopnjo napolnjenosti."
title = "Nastavitve {0}"
vehicle = "Vozilo"

View file

@ -211,8 +211,6 @@ solar = "Solenergi"
[main.loadpointSettings]
currents = "Laddningsström"
default = "förvald"
disclaimerHint = "Anmärkning:"
disclaimerText = "Ändringarna försvinner när servern startas om."
onlyForSocBasedCharging = "Dessa inställningar är endast tillgängliga för fordon med känd laddnivå"
title = "Inställningar {0}"
vehicle = "Fordon"

View file

@ -188,8 +188,6 @@ solar = "Сонячна"
[main.loadpointSettings]
currents = "Струм зарядки"
default = "за замовчуванням"
disclaimerHint = "Примітка:"
disclaimerText = "Зміни будуть втрачені після перезапуску сервера."
title = "Налаштування {0}"
vehicle = "Транспортний засіб"

View file

@ -198,8 +198,6 @@ solar = "太阳能"
[main.loadpointSettings]
currents = "充电货币"
default = "默认"
disclaimerHint = "笔记:"
disclaimerText = "服务器重新启动后,更改将丢失."
title = "设置 {0}"
vehicle = "车辆"

View file

@ -134,8 +134,8 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, cache *util.Cache) {
"mode": {[]string{"POST", "OPTIONS"}, "/mode/{value:[a-z]+}", chargeModeHandler(lp)},
"limitsoc": {[]string{"POST", "OPTIONS"}, "/limitsoc/{value:[0-9]+}", intHandler(pass(lp.SetLimitSoc), lp.GetLimitSoc)},
"limitenergy": {[]string{"POST", "OPTIONS"}, "/limitenergy/{value:[0-9.]+}", floatHandler(pass(lp.SetLimitEnergy), lp.GetLimitEnergy)},
"mincurrent": {[]string{"POST", "OPTIONS"}, "/mincurrent/{value:[0-9.]+}", floatHandler(pass(lp.SetMinCurrent), lp.GetMinCurrent)},
"maxcurrent": {[]string{"POST", "OPTIONS"}, "/maxcurrent/{value:[0-9.]+}", floatHandler(pass(lp.SetMaxCurrent), lp.GetMaxCurrent)},
"mincurrent": {[]string{"POST", "OPTIONS"}, "/mincurrent/{value:[0-9.]+}", floatHandler(lp.SetMinCurrent, lp.GetMinCurrent)},
"maxcurrent": {[]string{"POST", "OPTIONS"}, "/maxcurrent/{value:[0-9.]+}", floatHandler(lp.SetMaxCurrent, lp.GetMaxCurrent)},
"phases": {[]string{"POST", "OPTIONS"}, "/phases/{value:[0-9]+}", phasesHandler(lp)},
"plan": {[]string{"GET"}, "/plan", planHandler(lp)},
"planpreview": {[]string{"GET"}, "/plan/preview/{type:(?:soc|energy)}/{value:[0-9.]+}/{time:[0-9TZ:.-]+}", planPreviewHandler(lp)},

View file

@ -56,3 +56,18 @@ test.describe("session info", async () => {
await expect(page.getByTestId("sessionInfoLabel").first()).toContainText("Solar");
});
});
test.describe("loadpoint settings", async () => {
test("phase selection", async ({ page }) => {
await page.getByTestId("loadpoint-settings-button").nth(1).click();
await expect(page.getByLabel("auto-switching")).not.toBeVisible();
await expect(page.getByLabel("3 phase")).toBeChecked();
await expect(page.getByLabel("1 phase")).not.toBeChecked();
await expect(page.getByText("~ 11.0 kW")).toBeVisible();
await expect(page.getByText("~ 4.1 kW")).toBeVisible();
await page.getByLabel("1 phase").click();
await expect(page.getByText("~ 3.7 kW")).toBeVisible();
await expect(page.getByText("~ 1.4 kW")).toBeVisible();
});
});