Loadpoint: cleanup phase configuration and drop deprecations (#18638)

This commit is contained in:
andig 2025-02-09 09:12:55 +01:00 • committed by GitHub
parent f93ceffa6d
commit 07fb01d28c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 183 additions and 188 deletions

View file

@ -384,7 +384,7 @@
>
<SelectGroup
id="loadpointParamPhases"
v-model="values.phases"
v-model="values.phasesConfigured"
class="w-100"
:options="[
{ value: 1, name: $t('config.loadpoint.phases1p') },
@ -559,7 +559,7 @@ const nsPerMin = 60 * 1e9;
const defaultValues = {
title: "",
phases: 3,
phasesConfigured: 3,
minCurrent: 6,
maxCurrent: 16,
priority: 0,
@ -813,17 +813,17 @@ export default {
}
},
updatePhases() {
const { phases } = this.values;
const { phasesConfigured } = this.values;
if (this.chargerIsSinglePhase) {
this.values.phases = 1;
this.values.phasesConfigured = 1;
return;
}
if (this.chargerSupports1p3p && this.isNew) {
this.values.phases = 0; // automatic
this.values.phasesConfigured = 0; // automatic
return;
}
if (!this.chargerSupports1p3p && phases === 0) {
this.values.phases = 3; // no automatic switching, default to 3-phase
if (!this.chargerSupports1p3p && phasesConfigured === 0) {
this.values.phasesConfigured = 3; // no automatic switching, default to 3-phase
return;
}
},

View file

@ -180,7 +180,6 @@ export default {
chargeRemainingDuration: Number,
// other information
phases: Number,
phasesConfigured: Number,
phasesActive: Number,
chargerPhases1p3p: Boolean,

View file

@ -183,7 +183,6 @@ export default {
props: {
id: [String, Number],
phasesConfigured: Number,
phasesActive: Number,
chargerPhases1p3p: Boolean,
chargerSinglePhase: Boolean,
batteryBoost: Boolean,
@ -237,7 +236,7 @@ export default {
return this.maxPowerPhases(this.phasesConfigured);
}
}
return this.fmtW(this.maxCurrent * V * this.phasesActive);
return this.fmtW(this.maxCurrent * V * this.phasesConfigured);
},
minPower() {
if (this.chargerPhases1p3p) {
@ -248,7 +247,7 @@ export default {
return this.minPowerPhases(this.phasesConfigured);
}
}
return this.fmtW(this.minCurrent * V * this.phasesActive);
return this.fmtW(this.minCurrent * V * this.phasesConfigured);
},
minCurrentOptions() {
const opt1 = [...range(Math.floor(this.maxCurrent), 1), 0.5, 0.25, 0.125];

View file

@ -42,6 +42,7 @@
:key="loadpoint.name"
:name="loadpoint.title"
:editable="!!loadpoint.id"
:error="deviceError('loadpoint', loadpoint.name)"
data-testid="loadpoint"
@edit="editLoadpoint(loadpoint.id)"
>

View file

@ -7,11 +7,11 @@ import (
"strings"
)
const _ClassName = "configfilemeterchargervehicletariffcircuitsitemqttdatabasemodbusproxyeebusjavascriptgohemsinfluxmessengersponsorship"
const _ClassName = "configfilemeterchargervehicletariffcircuitsitemqttdatabasemodbusproxyeebusjavascriptgohemsinfluxmessengersponsorshiploadpoint"
var _ClassIndex = [...]uint8{0, 10, 15, 22, 29, 35, 42, 46, 50, 58, 69, 74, 84, 86, 90, 96, 105, 116}
var _ClassIndex = [...]uint8{0, 10, 15, 22, 29, 35, 42, 46, 50, 58, 69, 74, 84, 86, 90, 96, 105, 116, 125}
const _ClassLowerName = "configfilemeterchargervehicletariffcircuitsitemqttdatabasemodbusproxyeebusjavascriptgohemsinfluxmessengersponsorship"
const _ClassLowerName = "configfilemeterchargervehicletariffcircuitsitemqttdatabasemodbusproxyeebusjavascriptgohemsinfluxmessengersponsorshiploadpoint"
func (i Class) String() string {
i -= 1
@ -42,9 +42,10 @@ func _ClassNoOp() {
_ = x[ClassInflux-(15)]
_ = x[ClassMessenger-(16)]
_ = x[ClassSponsorship-(17)]
_ = x[ClassLoadpoint-(18)]
}
var _ClassValues = []Class{ClassConfigFile, ClassMeter, ClassCharger, ClassVehicle, ClassTariff, ClassCircuit, ClassSite, ClassMqtt, ClassDatabase, ClassModbusProxy, ClassEEBus, ClassJavascript, ClassGo, ClassHEMS, ClassInflux, ClassMessenger, ClassSponsorship}
var _ClassValues = []Class{ClassConfigFile, ClassMeter, ClassCharger, ClassVehicle, ClassTariff, ClassCircuit, ClassSite, ClassMqtt, ClassDatabase, ClassModbusProxy, ClassEEBus, ClassJavascript, ClassGo, ClassHEMS, ClassInflux, ClassMessenger, ClassSponsorship, ClassLoadpoint}
var _ClassNameToValueMap = map[string]Class{
_ClassName[0:10]: ClassConfigFile,
@ -81,6 +82,8 @@ var _ClassNameToValueMap = map[string]Class{
_ClassLowerName[96:105]: ClassMessenger,
_ClassName[105:116]: ClassSponsorship,
_ClassLowerName[105:116]: ClassSponsorship,
_ClassName[116:125]: ClassLoadpoint,
_ClassLowerName[116:125]: ClassLoadpoint,
}
var _ClassNames = []string{
@ -101,6 +104,7 @@ var _ClassNames = []string{
_ClassName[90:96],
_ClassName[96:105],
_ClassName[105:116],
_ClassName[116:125],
}
// ClassString retrieves an enum value from the enum constants string name.

View file

@ -27,6 +27,7 @@ const (
ClassInflux
ClassMessenger
ClassSponsorship
ClassLoadpoint
)
// FatalError is an error that can be marshaled
@ -57,7 +58,7 @@ type DeviceError struct {
}
func (e *DeviceError) Error() string {
return e.err.Error()
return "[" + e.Name + "] " + e.err.Error()
}
// ClassError indicates the class of devices that failed
@ -67,7 +68,7 @@ type ClassError struct {
}
func (e *ClassError) Error() string {
return e.err.Error()
return e.Class.String() + " " + e.err.Error()
}
func (e ClassError) MarshalJSON() ([]byte, error) {

View file

@ -18,5 +18,5 @@ func TestError(t *testing.T) {
b, err := res.MarshalJSON()
require.NoError(t, err)
require.Equal(t, `{"class":"meter","device":"0815","error":"foo"}`, string(b))
require.Equal(t, `{"class":"meter","device":"0815","error":"[0815] foo"}`, string(b))
}

View file

@ -862,7 +862,7 @@ func configureSiteAndLoadpoints(conf *globalconfig.All) (*core.Site, error) {
}
if err := configureLoadpoints(*conf); err != nil {
return nil, fmt.Errorf("failed configuring loadpoints: %w", err)
return nil, &ClassError{ClassLoadpoint, err}
}
tariffs, err := configureTariffs(conf.Tariffs)
@ -945,7 +945,7 @@ func configureLoadpoints(conf globalconfig.All) error {
instance, err := core.NewLoadpointFromConfig(log, settings, cc.Other)
if err != nil {
return fmt.Errorf("failed configuring loadpoint: %w", err)
return &DeviceError{cc.Name, err}
}
if err := config.Loadpoints().Add(config.NewStaticDevice(cc, loadpoint.API(instance))); err != nil {
@ -970,21 +970,21 @@ func configureLoadpoints(conf globalconfig.All) error {
dynamic, static, err := loadpoint.SplitConfig(cc.Other)
if err != nil {
return fmt.Errorf("failed configuring loadpoint: %w", err)
return &DeviceError{cc.Name, err}
}
instance, err := core.NewLoadpointFromConfig(log, settings, static)
if err != nil {
return fmt.Errorf("failed configuring loadpoint: %w", err)
}
if err := dynamic.Apply(instance); err != nil {
return err
return &DeviceError{cc.Name, err}
}
dev := config.NewConfigurableDevice[loadpoint.API](&conf, instance)
if err := config.Loadpoints().Add(dev); err != nil {
return err
return &DeviceError{cc.Name, err}
}
if err := dynamic.Apply(instance); err != nil {
return &DeviceError{cc.Name, err}
}
}

View file

@ -19,9 +19,7 @@ const (
DisableDelay = "disableDelay"
BatteryBoost = "batteryBoost"
Phases = "phases" // configured phases (1/3, 0 for auto on 1p3p chargers, nil for plain chargers)
PhasesConfigured = "phasesConfigured" // TODO mirrors "phases" for UI purposes
PhasesEnabled = "phasesEnabled" // enabled phases (1/3)
PhasesConfigured = "phasesConfigured" // desired phase mode (0/1/3, 0 = automatic), user selection
PhasesActive = "phasesActive" // active phases as used by vehicle (1/2/3)
ChargerIcon = "chargerIcon" // charger icon for ui

View file

@ -93,20 +93,22 @@ type Loadpoint struct {
Soc loadpoint.SocConfig
Enable, Disable loadpoint.ThresholdConfig
// TODO deprecated
// from yaml
DefaultMode api.ChargeMode `mapstructure:"mode"` // Default charge mode, used for disconnect
Title_ string `mapstructure:"title"` // UI title
Priority_ int `mapstructure:"priority"` // Priority
GuardDuration_ time.Duration `mapstructure:"guardduration"` // charger enable/disable minimum holding time
Phases_ int `mapstructure:"phases"`
MinCurrent_ float64 `mapstructure:"minCurrent"`
MaxCurrent_ float64 `mapstructure:"maxCurrent"`
Title string `mapstructure:"title"` // UI title
Priority int `mapstructure:"priority"` // Priority
// from yaml, deprecated
GuardDuration_ time.Duration `mapstructure:"guardduration"` // ignored, present for compatibility
Phases_ int `mapstructure:"phases"` // ignored, present for compatibility
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
configuredPhases int // Charger configured phase mode 0/1/3
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
@ -223,13 +225,10 @@ func NewLoadpointFromConfig(log *util.Logger, settings settings.Settings, other
// phase switching defaults based on charger capabilities
if !lp.hasPhaseSwitching() {
lp.configuredPhases = 3
lp.phasesConfigured = 3
lp.phases = 3
}
// TODO deprecated
lp.migrateSettings()
// validate thresholds
if lp.Enable.Threshold > lp.Disable.Threshold {
lp.log.WARN.Printf("PV mode enable threshold (%.0fW) is larger than disable threshold (%.0fW)", lp.Enable.Threshold, lp.Disable.Threshold)
@ -275,73 +274,43 @@ func NewLoadpoint(log *util.Logger, settings settings.Settings) *Loadpoint {
return lp
}
// migrateSettings migrates loadpoint settings
func (lp *Loadpoint) migrateSettings() {
// One-time migrations MUST be mirrored in restoreSettings
if lp.DefaultMode != "" {
lp.log.WARN.Println("deprecated: mode setting is ignored, please remove")
if _, err := lp.settings.String(keys.Mode); err != nil {
lp.settings.SetString(keys.Mode, string(lp.DefaultMode))
}
}
if lp.Title_ != "" {
lp.log.WARN.Println("deprecated: title setting is ignored, please remove")
if _, err := lp.settings.String(keys.Title); err != nil {
lp.settings.SetString(keys.Title, lp.Title_)
}
}
if lp.Priority_ > 0 {
lp.log.WARN.Println("deprecated: priority setting is ignored, please remove")
if _, err := lp.settings.String(keys.Priority); err != nil {
lp.settings.SetInt(keys.Priority, int64(lp.Priority_))
}
}
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.Phases_ > 0 {
lp.log.WARN.Println("deprecated: phases setting is ignored, please remove")
if _, err := lp.settings.Int(keys.Phases); err != nil {
lp.settings.SetInt(keys.Phases, int64(lp.Phases_))
}
}
if lp.Soc.Estimate != nil || lp.Soc.Poll.Mode != loadpoint.PollCharging || lp.Soc.Poll.Interval != 0 {
lp.log.WARN.Println("deprecated: soc setting is ignored, please remove")
if _, err := lp.settings.String(keys.Soc); err != nil {
lp.settings.SetJson(keys.Soc, lp.Soc)
}
}
}
// restoreSettings restores loadpoint settings
func (lp *Loadpoint) restoreSettings() {
if testing.Testing() {
return
}
if v, err := lp.settings.String(keys.Title); err == nil && v != "" {
lp.setTitle(v)
// from yaml
if lp.Title != "" {
lp.setTitle(lp.Title)
}
if lp.Priority > 0 {
lp.setPriority(lp.Priority)
}
// deprecated yaml properties
if lp.Phases_ > 0 {
lp.log.WARN.Printf("ignoring deprecated phases: %d. please configure via UI", lp.Phases_)
}
if lp.MinCurrent_ > 0 {
lp.log.WARN.Printf("ignoring deprecated minCurrent: %f. please configure via UI", lp.MinCurrent_)
}
if lp.MaxCurrent_ > 0 {
lp.log.WARN.Printf("ignoring deprecated maxCurrent: %f. please configure via UI", lp.MaxCurrent_)
}
if lp.GuardDuration_ > 0 {
lp.log.WARN.Printf("ignoring deprecated guardduration: %s. please configure via UI", lp.GuardDuration_)
}
// restore runtime configuration (database & yaml LPs)
if v, err := lp.settings.String(keys.Mode); err == nil && v != "" {
lp.setMode(api.ChargeMode(v))
}
if v, err := lp.settings.Int(keys.Priority); err == nil && v > 0 {
lp.setPriority(int(v))
}
if v, err := lp.settings.Int(keys.Phases); err == nil && (v > 0 || lp.hasPhaseSwitching()) {
lp.setConfiguredPhases(int(v))
// for 1p3p charger, we don't know the physical state yet (phases == 0), so don't touch it
if !lp.hasPhaseSwitching() {
lp.phases = lp.configuredPhases
}
if v, err := lp.settings.Int(keys.PhasesConfigured); err == nil && (v > 0 || lp.hasPhaseSwitching()) {
lp.setPhasesConfigured(int(v))
}
if v, err := lp.settings.Float(keys.MinCurrent); err == nil && v > 0 {
lp.setMinCurrent(v)
@ -534,7 +503,7 @@ func (lp *Loadpoint) evVehicleDisconnectHandler() {
lp.clearSession()
// phases are unknown when vehicle disconnects
lp.resetMeasuredPhases()
lp.ResetMeasuredPhases()
// energy and duration
lp.energyMetrics.Publish("session", lp)
@ -647,20 +616,16 @@ func (lp *Loadpoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
lp.publish(keys.DisableDelay, lp.Disable.Delay)
if phases := lp.getChargerPhysicalPhases(); phases != 0 {
if lp.configuredPhases != phases && lp.configuredPhases != 0 {
lp.log.WARN.Printf("configured phases %d do not match physical phases %d", lp.configuredPhases, phases)
if lp.phasesConfigured != phases && lp.phasesConfigured != 0 {
lp.log.WARN.Printf("configured phases %d do not match physical phases %d", lp.phasesConfigured, phases)
}
lp.phases = phases
lp.configuredPhases = phases
lp.publish(keys.Phases, phases)
} else {
lp.publish(keys.Phases, nil)
lp.phasesConfigured = phases
}
lp.publish(keys.PhasesConfigured, lp.configuredPhases)
lp.publish(keys.PhasesConfigured, lp.phasesConfigured)
lp.publish(keys.ChargerPhases1p3p, lp.hasPhaseSwitching())
lp.publish(keys.ChargerSinglePhase, lp.getChargerPhysicalPhases() == 1)
lp.publish(keys.PhasesEnabled, lp.phases)
lp.publish(keys.PhasesActive, lp.ActivePhases())
lp.publish(keys.SmartCostLimit, lp.smartCostLimit)
lp.publishTimer(phaseTimer, 0, timerInactive)
@ -815,7 +780,7 @@ func (lp *Loadpoint) syncCharger() error {
if chargerPhases, err = pg.GetPhases(); err == nil {
if chargerPhases > 0 && chargerPhases != phases {
lp.log.WARN.Printf("charger logic error: phases mismatch (got %d, expected %d)", chargerPhases, phases)
lp.setPhases(chargerPhases)
lp.SetPhases(chargerPhases)
}
} else {
if errors.Is(err, api.ErrNotAvailable) {
@ -829,7 +794,7 @@ func (lp *Loadpoint) syncCharger() error {
if !isPg || errors.Is(err, api.ErrNotAvailable) {
if chargerPhases > phases {
lp.log.WARN.Printf("charger logic error: phases mismatch (got %d measured, expected %d)", chargerPhases, phases)
lp.setPhases(chargerPhases)
lp.SetPhases(chargerPhases)
}
}
}
@ -1188,13 +1153,13 @@ func (lp *Loadpoint) resetPhaseTimer() {
// scalePhasesRequired validates if fixed phase configuration matches enabled phases
func (lp *Loadpoint) scalePhasesRequired() bool {
return lp.hasPhaseSwitching() && lp.configuredPhases != 0 && lp.configuredPhases != lp.GetPhases()
return lp.hasPhaseSwitching() && lp.phasesConfigured != 0 && lp.phasesConfigured != 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.phasesConfigured != 0 {
phases = lp.phasesConfigured
}
if lp.hasPhaseSwitching() {
@ -1224,7 +1189,7 @@ func (lp *Loadpoint) scalePhases(phases int) error {
lp.phasesSwitched = lp.clock.Now()
// update setting and reset timer
lp.setPhases(phases)
lp.SetPhases(phases)
}
return nil
@ -1252,13 +1217,13 @@ func (lp *Loadpoint) pvScalePhases(sitePower, minCurrent, maxCurrent float64) in
if lp.chargerUpdateCompleted() && lp.phaseSwitchCompleted() {
lp.log.WARN.Printf("ignoring inconsistent phases: %dp < %dp observed active", phases, measuredPhases)
}
lp.resetMeasuredPhases()
lp.ResetMeasuredPhases()
}
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.phasesConfigured < 3
lp.log.DEBUG.Printf("!! pvScalePhases DOWN activePhases: %d, available power: %.0fW, scalable: %t", activePhases, availablePower, scalable)
@ -1604,7 +1569,7 @@ func (lp *Loadpoint) updateChargeVoltages() {
if phases >= 1 {
lp.log.DEBUG.Printf("detected connected phases: %dp", phases)
lp.setPhases(phases)
lp.SetPhases(phases)
}
}
@ -1852,7 +1817,7 @@ func (lp *Loadpoint) Update(sitePower, batteryBoostPower float64, rates api.Rate
err = lp.setLimit(0)
case lp.scalePhasesRequired():
err = lp.scalePhases(lp.configuredPhases)
err = lp.scalePhases(lp.phasesConfigured)
case lp.remoteControlled(loadpoint.RemoteHardDisable):
remoteDisabled = loadpoint.RemoteHardDisable

View file

@ -68,8 +68,10 @@ type API interface {
SetDefaultMode(api.ChargeMode)
// GetPhases returns the enabled phases
GetPhases() int
// SetPhases sets the enabled phases
SetPhases(int) error
// GetPhasesConfigured returns the configured phases
GetPhasesConfigured() int
// SetPhasesConfigured sets the configured phases
SetPhasesConfigured(int) error
// ActivePhases returns the active phases for the current vehicle
ActivePhases() int

View file

@ -20,7 +20,7 @@ type DynamicConfig struct {
Title string `json:"title"`
DefaultMode string `json:"defaultMode"`
Priority int `json:"priority"`
Phases int `json:"phases"`
PhasesConfigured int `json:"phasesConfigured"`
MinCurrent float64 `json:"minCurrent"`
MaxCurrent float64 `json:"maxCurrent"`
SmartCostLimit *float64 `json:"smartCostLimit"`
@ -69,7 +69,7 @@ func (payload DynamicConfig) Apply(lp API) error {
}
if err == nil {
err = lp.SetPhases(payload.Phases)
err = lp.SetPhasesConfigured(payload.PhasesConfigured)
}
if err == nil && payload.MinCurrent != 0 {

View file

@ -405,6 +405,20 @@ func (mr *MockAPIMockRecorder) GetPhases() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPhases", reflect.TypeOf((*MockAPI)(nil).GetPhases))
}
// GetPhasesConfigured mocks base method.
func (m *MockAPI) GetPhasesConfigured() int {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPhasesConfigured")
ret0, _ := ret[0].(int)
return ret0
}
// GetPhasesConfigured indicates an expected call of GetPhasesConfigured.
func (mr *MockAPIMockRecorder) GetPhasesConfigured() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPhasesConfigured", reflect.TypeOf((*MockAPI)(nil).GetPhasesConfigured))
}
// GetPlan mocks base method.
func (m *MockAPI) GetPlan(targetTime time.Time, requiredDuration time.Duration) (api.Rates, error) {
m.ctrl.T.Helper()
@ -780,18 +794,18 @@ func (mr *MockAPIMockRecorder) SetMode(arg0 any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMode", reflect.TypeOf((*MockAPI)(nil).SetMode), arg0)
}
// SetPhases mocks base method.
func (m *MockAPI) SetPhases(arg0 int) error {
// SetPhasesConfigured mocks base method.
func (m *MockAPI) SetPhasesConfigured(arg0 int) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetPhases", arg0)
ret := m.ctrl.Call(m, "SetPhasesConfigured", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// SetPhases indicates an expected call of SetPhases.
func (mr *MockAPIMockRecorder) SetPhases(arg0 any) *gomock.Call {
// SetPhasesConfigured indicates an expected call of SetPhasesConfigured.
func (mr *MockAPIMockRecorder) SetPhasesConfigured(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPhases", reflect.TypeOf((*MockAPI)(nil).SetPhases), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPhasesConfigured", reflect.TypeOf((*MockAPI)(nil).SetPhasesConfigured), arg0)
}
// SetPlanEnergy mocks base method.

View file

@ -166,25 +166,27 @@ func (lp *Loadpoint) SetPriority(prio int) {
lp.log.DEBUG.Println("set priority:", prio)
if lp.Priority_ != prio {
if lp.Priority != prio {
lp.setPriority(prio)
}
}
// GetPhases returns loadpoint enabled phases
// GetPhases returns the enabled phases
func (lp *Loadpoint) GetPhases() int {
lp.RLock()
defer lp.RUnlock()
return lp.getPhases()
}
// getPhases returns loadpoint enabled phases
func (lp *Loadpoint) getPhases() int {
return lp.phases
}
// SetPhases sets loadpoint enabled phases
func (lp *Loadpoint) SetPhases(phases int) error {
// GetPhasesConfigured returns the configured phases
func (lp *Loadpoint) GetPhasesConfigured() int {
lp.RLock()
defer lp.RUnlock()
return lp.phasesConfigured
}
// SetPhasesConfigured sets the configured phases
func (lp *Loadpoint) SetPhasesConfigured(phases int) error {
// limit auto mode (phases=0) to scalable charger
if !lp.hasPhaseSwitching() && phases == 0 {
return fmt.Errorf("charger does not support phase switching")
@ -198,13 +200,13 @@ func (lp *Loadpoint) SetPhases(phases int) error {
lp.log.DEBUG.Println("set phases:", phases)
lp.Lock()
lp.setConfiguredPhases(phases)
lp.Unlock()
lp.setPhasesConfigured(phases)
// apply immediately if not 1p3p
if !lp.hasPhaseSwitching() {
lp.setPhases(phases)
}
lp.Unlock()
lp.requestUpdate()

View file

@ -5,23 +5,30 @@ import (
"github.com/evcc-io/evcc/core/keys"
)
// setConfiguredPhases sets the default phase configuration
func (lp *Loadpoint) setConfiguredPhases(phases int) {
lp.configuredPhases = phases
lp.publish(keys.Phases, lp.configuredPhases)
lp.publish(keys.PhasesConfigured, lp.configuredPhases) // TODO remove
lp.settings.SetInt(keys.Phases, int64(lp.configuredPhases))
// setPhasesConfigured sets the default phase configuration
func (lp *Loadpoint) setPhasesConfigured(phases int) {
lp.phasesConfigured = phases
lp.publish(keys.PhasesConfigured, lp.phasesConfigured)
lp.settings.SetInt(keys.PhasesConfigured, int64(lp.phasesConfigured))
// configured phases are actual phases for non-1p3p charger
// for 1p3p charger, configuration does not mean that the physical state has changed, so don't touch it
if !lp.hasPhaseSwitching() {
lp.setPhases(phases)
}
}
// SetPhases sets the number of enabled phases without modifying the charger
func (lp *Loadpoint) SetPhases(phases int) {
lp.Lock()
defer lp.Unlock()
lp.setPhases(phases)
}
// setPhases sets the number of enabled phases without modifying the charger
func (lp *Loadpoint) setPhases(phases int) {
if lp.GetPhases() != phases {
lp.Lock()
if lp.phases != phases {
lp.phases = phases
lp.Unlock()
// publish updated phase configuration
lp.publish(keys.PhasesEnabled, lp.phases)
// reset timer to disabled state
lp.resetPhaseTimer()
@ -31,13 +38,17 @@ func (lp *Loadpoint) setPhases(phases int) {
}
}
// ResetMeasuredPhases resets measured phases to unknown on vehicle disconnect, phase switch or phase api call
func (lp *Loadpoint) ResetMeasuredPhases() {
lp.Lock()
defer lp.Unlock()
lp.resetMeasuredPhases()
}
// resetMeasuredPhases resets measured phases to unknown on vehicle disconnect, phase switch or phase api call
func (lp *Loadpoint) resetMeasuredPhases() {
lp.Lock()
lp.measuredPhases = 0
lp.Unlock()
lp.publish(keys.PhasesActive, lp.ActivePhases())
lp.publish(keys.PhasesActive, lp.activePhases())
}
// GetMeasuredPhases provides synchronized access to measuredPhases
@ -73,7 +84,7 @@ func (lp *Loadpoint) ActivePhases() int {
// activePhases returns the number of expectedly active phases for the meter.
// If unknown for 1p3p chargers during startup it will assume 3p.
func (lp *Loadpoint) activePhases() int {
physical := lp.getPhases()
physical := lp.phases
vehicle := lp.getVehiclePhases()
measured := lp.getMeasuredPhases()
charger := lp.getChargerPhysicalPhases()
@ -97,8 +108,7 @@ func (lp *Loadpoint) MinActivePhases() int {
// minActivePhases returns the minimum number of active phases for the loadpoint.
func (lp *Loadpoint) minActivePhases() int {
// 1p3p supported or limit 1p
if lp.hasPhaseSwitching() || lp.configuredPhases == 1 {
if lp.hasPhaseSwitching() || lp.phasesConfigured == 1 {
return 1
}
@ -114,7 +124,7 @@ func (lp *Loadpoint) MaxActivePhases() int {
// maxActivePhases returns the maximum number of active phases for the loadpoint.
func (lp *Loadpoint) maxActivePhases() int {
physical := lp.getPhases()
physical := lp.phases
measured := lp.getMeasuredPhases()
vehicle := lp.getVehiclePhases()
charger := lp.getChargerPhysicalPhases()
@ -126,7 +136,7 @@ func (lp *Loadpoint) maxActivePhases() int {
// if 1p3p supported then assume configured limit or 3p
if lp.hasPhaseSwitching() {
physical = lp.configuredPhases
physical = lp.phasesConfigured
}
return min(expect(vehicle), expect(physical), expect(measured), expect(charger))

View file

@ -82,7 +82,7 @@ func TestMaxActivePhases(t *testing.T) {
vehicle.EXPECT().Phases().Return(tc.vehicle).MinTimes(1)
lp := &Loadpoint{
configuredPhases: configured, // fixed phases or default
phasesConfigured: configured, // fixed phases or default
vehicle: vehicle,
phases: tc.physical,
measuredPhases: tc.measuredPhases,
@ -134,7 +134,7 @@ func TestMinActivePhases(t *testing.T) {
vehicle.EXPECT().Phases().Return(tc.vehicle).AnyTimes()
lp := &Loadpoint{
configuredPhases: configured, // fixed phases or default
phasesConfigured: configured, // fixed phases or default
vehicle: vehicle,
phases: tc.physical,
measuredPhases: tc.measuredPhases,
@ -231,7 +231,7 @@ func TestPvScalePhases(t *testing.T) {
minCurrent: minA,
maxCurrent: maxA,
vehicle: vehicle,
configuredPhases: 0, // allow switching
phasesConfigured: 0, // allow switching
phases: tc.physical,
status: api.StatusC,
}
@ -463,7 +463,7 @@ func TestScalePhasesIfAvailable(t *testing.T) {
phaseCharger,
},
minCurrent: minA,
configuredPhases: tc.dflt, // fixed phases or default
phasesConfigured: tc.dflt, // fixed phases or default
phases: tc.physical, // current phase status
}

View file

@ -166,7 +166,7 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, valueChan chan<- util.Param)
"limitenergy": {"POST", "/limitenergy/{value:[0-9.]+}", floatHandler(pass(lp.SetLimitEnergy), lp.GetLimitEnergy)},
"mincurrent": {"POST", "/mincurrent/{value:[0-9.]+}", floatHandler(lp.SetMinCurrent, lp.GetMinCurrent)},
"maxcurrent": {"POST", "/maxcurrent/{value:[0-9.]+}", floatHandler(lp.SetMaxCurrent, lp.GetMaxCurrent)},
"phases": {"POST", "/phases/{value:[0-9]+}", intHandler(lp.SetPhases, lp.GetPhases)},
"phases": {"POST", "/phases/{value:[0-9]+}", intHandler(lp.SetPhasesConfigured, lp.GetPhasesConfigured)},
"plan": {"GET", "/plan", planHandler(lp)},
"staticPlanPreview": {"GET", "/plan/static/preview/{type:(?:soc|energy)}/{value:[0-9.]+}/{time:[0-9TZ:.+-]+}", staticPlanPreviewHandler(lp)},
"repeatingPlanPreview": {"GET", "/plan/repeating/preview/{soc:[0-9]+}/{weekdays:[0-6,]+}/{time:[0-2][0-9]:[0-5][0-9]}/{tz:[a-zA-Z0-9_./:-]+}", repeatingPlanPreviewHandler(lp)},

View file

@ -31,7 +31,7 @@ func getLoadpointDynamicConfig(lp loadpoint.API) loadpoint.DynamicConfig {
Title: lp.GetTitle(),
DefaultMode: string(lp.GetDefaultMode()),
Priority: lp.GetPriority(),
Phases: lp.GetPhases(),
PhasesConfigured: lp.GetPhasesConfigured(),
MinCurrent: lp.GetMinCurrent(),
MaxCurrent: lp.GetMaxCurrent(),
SmartCostLimit: lp.GetSmartCostLimit(),

View file

@ -201,7 +201,7 @@ func (m *MQTT) listenSiteSetters(topic string, site site.API) error {
func (m *MQTT) listenLoadpointSetters(topic string, site site.API, lp loadpoint.API) error {
for _, s := range []setter{
{"mode", setterFunc(api.ChargeModeString, pass(lp.SetMode))},
{"phases", intSetter(lp.SetPhases)},
{"phases", intSetter(lp.SetPhasesConfigured)},
{"limitSoc", intSetter(pass(lp.SetLimitSoc))},
{"priority", intSetter(pass(lp.SetPriority))},
{"minCurrent", floatSetter(lp.SetMinCurrent)},