Load Management and Peak Shaving (#13207)
This commit is contained in:
parent
652cab3192
commit
26973a0d2c
24 changed files with 1339 additions and 49 deletions
31
api/api.go
31
api/api.go
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,PhaseSwitcher,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery,Tariff,BatteryController
|
//go:generate mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,PhaseSwitcher,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ChargeRater,Battery,Tariff,BatteryController,Circuit
|
||||||
|
|
||||||
// Meter provides total active power in W
|
// Meter provides total active power in W
|
||||||
type Meter interface {
|
type Meter interface {
|
||||||
|
|
@ -202,3 +202,32 @@ type FeatureDescriber interface {
|
||||||
type CsvWriter interface {
|
type CsvWriter interface {
|
||||||
WriteCsv(context.Context, io.Writer) error
|
WriteCsv(context.Context, io.Writer) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CircuitMeasurements is the measurements a circuit or load must deliver
|
||||||
|
type CircuitMeasurements interface {
|
||||||
|
GetChargePower() float64
|
||||||
|
GetMaxPhaseCurrent() float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// CircuitLoad represents a loadpoint attached to a circuit
|
||||||
|
type CircuitLoad interface {
|
||||||
|
CircuitMeasurements
|
||||||
|
GetCircuit() Circuit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Circuit defines the load control domain
|
||||||
|
type Circuit interface {
|
||||||
|
CircuitMeasurements
|
||||||
|
GetTitle() string
|
||||||
|
SetTitle(string)
|
||||||
|
GetParent() Circuit
|
||||||
|
RegisterChild(child Circuit)
|
||||||
|
HasMeter() bool
|
||||||
|
GetMaxPower() float64
|
||||||
|
GetMaxCurrent() float64
|
||||||
|
SetMaxPower(float64)
|
||||||
|
SetMaxCurrent(float64)
|
||||||
|
Update([]CircuitLoad) error
|
||||||
|
ValidateCurrent(old, new float64) float64
|
||||||
|
ValidatePower(old, new float64) float64
|
||||||
|
}
|
||||||
|
|
|
||||||
255
api/mock.go
255
api/mock.go
|
|
@ -1,9 +1,9 @@
|
||||||
// Code generated by MockGen. DO NOT EDIT.
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
// Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,CurrentLimiter,PhaseSwitcher,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery,Tariff,BatteryController)
|
// Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,CurrentLimiter,PhaseSwitcher,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ChargeRater,Battery,Tariff,BatteryController,Circuit)
|
||||||
//
|
//
|
||||||
// Generated by this command:
|
// Generated by this command:
|
||||||
//
|
//
|
||||||
// mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,PhaseSwitcher,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery,Tariff,BatteryController
|
// mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,PhaseSwitcher,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ChargeRater,Battery,Tariff,BatteryController,Circuit
|
||||||
//
|
//
|
||||||
|
|
||||||
// Package api is a generated GoMock package.
|
// Package api is a generated GoMock package.
|
||||||
|
|
@ -324,6 +324,46 @@ func (mr *MockMeterEnergyMockRecorder) TotalEnergy() *gomock.Call {
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TotalEnergy", reflect.TypeOf((*MockMeterEnergy)(nil).TotalEnergy))
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TotalEnergy", reflect.TypeOf((*MockMeterEnergy)(nil).TotalEnergy))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MockPhaseCurrents is a mock of PhaseCurrents interface.
|
||||||
|
type MockPhaseCurrents struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockPhaseCurrentsMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockPhaseCurrentsMockRecorder is the mock recorder for MockPhaseCurrents.
|
||||||
|
type MockPhaseCurrentsMockRecorder struct {
|
||||||
|
mock *MockPhaseCurrents
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockPhaseCurrents creates a new mock instance.
|
||||||
|
func NewMockPhaseCurrents(ctrl *gomock.Controller) *MockPhaseCurrents {
|
||||||
|
mock := &MockPhaseCurrents{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockPhaseCurrentsMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||||
|
func (m *MockPhaseCurrents) EXPECT() *MockPhaseCurrentsMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Currents mocks base method.
|
||||||
|
func (m *MockPhaseCurrents) Currents() (float64, float64, float64, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Currents")
|
||||||
|
ret0, _ := ret[0].(float64)
|
||||||
|
ret1, _ := ret[1].(float64)
|
||||||
|
ret2, _ := ret[2].(float64)
|
||||||
|
ret3, _ := ret[3].(error)
|
||||||
|
return ret0, ret1, ret2, ret3
|
||||||
|
}
|
||||||
|
|
||||||
|
// Currents indicates an expected call of Currents.
|
||||||
|
func (mr *MockPhaseCurrentsMockRecorder) Currents() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Currents", reflect.TypeOf((*MockPhaseCurrents)(nil).Currents))
|
||||||
|
}
|
||||||
|
|
||||||
// MockVehicle is a mock of Vehicle interface.
|
// MockVehicle is a mock of Vehicle interface.
|
||||||
type MockVehicle struct {
|
type MockVehicle struct {
|
||||||
ctrl *gomock.Controller
|
ctrl *gomock.Controller
|
||||||
|
|
@ -636,3 +676,214 @@ func (mr *MockBatteryControllerMockRecorder) SetBatteryMode(arg0 any) *gomock.Ca
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBatteryMode", reflect.TypeOf((*MockBatteryController)(nil).SetBatteryMode), arg0)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBatteryMode", reflect.TypeOf((*MockBatteryController)(nil).SetBatteryMode), arg0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MockCircuit is a mock of Circuit interface.
|
||||||
|
type MockCircuit struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockCircuitMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockCircuitMockRecorder is the mock recorder for MockCircuit.
|
||||||
|
type MockCircuitMockRecorder struct {
|
||||||
|
mock *MockCircuit
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockCircuit creates a new mock instance.
|
||||||
|
func NewMockCircuit(ctrl *gomock.Controller) *MockCircuit {
|
||||||
|
mock := &MockCircuit{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockCircuitMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||||
|
func (m *MockCircuit) EXPECT() *MockCircuitMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChargePower mocks base method.
|
||||||
|
func (m *MockCircuit) GetChargePower() float64 {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetChargePower")
|
||||||
|
ret0, _ := ret[0].(float64)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChargePower indicates an expected call of GetChargePower.
|
||||||
|
func (mr *MockCircuitMockRecorder) GetChargePower() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChargePower", reflect.TypeOf((*MockCircuit)(nil).GetChargePower))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMaxCurrent mocks base method.
|
||||||
|
func (m *MockCircuit) GetMaxCurrent() float64 {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetMaxCurrent")
|
||||||
|
ret0, _ := ret[0].(float64)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMaxCurrent indicates an expected call of GetMaxCurrent.
|
||||||
|
func (mr *MockCircuitMockRecorder) GetMaxCurrent() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaxCurrent", reflect.TypeOf((*MockCircuit)(nil).GetMaxCurrent))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMaxPhaseCurrent mocks base method.
|
||||||
|
func (m *MockCircuit) GetMaxPhaseCurrent() float64 {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetMaxPhaseCurrent")
|
||||||
|
ret0, _ := ret[0].(float64)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMaxPhaseCurrent indicates an expected call of GetMaxPhaseCurrent.
|
||||||
|
func (mr *MockCircuitMockRecorder) GetMaxPhaseCurrent() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaxPhaseCurrent", reflect.TypeOf((*MockCircuit)(nil).GetMaxPhaseCurrent))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMaxPower mocks base method.
|
||||||
|
func (m *MockCircuit) GetMaxPower() float64 {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetMaxPower")
|
||||||
|
ret0, _ := ret[0].(float64)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMaxPower indicates an expected call of GetMaxPower.
|
||||||
|
func (mr *MockCircuitMockRecorder) GetMaxPower() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaxPower", reflect.TypeOf((*MockCircuit)(nil).GetMaxPower))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetParent mocks base method.
|
||||||
|
func (m *MockCircuit) GetParent() Circuit {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetParent")
|
||||||
|
ret0, _ := ret[0].(Circuit)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetParent indicates an expected call of GetParent.
|
||||||
|
func (mr *MockCircuitMockRecorder) GetParent() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetParent", reflect.TypeOf((*MockCircuit)(nil).GetParent))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTitle mocks base method.
|
||||||
|
func (m *MockCircuit) GetTitle() string {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetTitle")
|
||||||
|
ret0, _ := ret[0].(string)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTitle indicates an expected call of GetTitle.
|
||||||
|
func (mr *MockCircuitMockRecorder) GetTitle() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTitle", reflect.TypeOf((*MockCircuit)(nil).GetTitle))
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasMeter mocks base method.
|
||||||
|
func (m *MockCircuit) HasMeter() bool {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "HasMeter")
|
||||||
|
ret0, _ := ret[0].(bool)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasMeter indicates an expected call of HasMeter.
|
||||||
|
func (mr *MockCircuitMockRecorder) HasMeter() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasMeter", reflect.TypeOf((*MockCircuit)(nil).HasMeter))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterChild mocks base method.
|
||||||
|
func (m *MockCircuit) RegisterChild(arg0 Circuit) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
m.ctrl.Call(m, "RegisterChild", arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterChild indicates an expected call of RegisterChild.
|
||||||
|
func (mr *MockCircuitMockRecorder) RegisterChild(arg0 any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterChild", reflect.TypeOf((*MockCircuit)(nil).RegisterChild), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMaxCurrent mocks base method.
|
||||||
|
func (m *MockCircuit) SetMaxCurrent(arg0 float64) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
m.ctrl.Call(m, "SetMaxCurrent", arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMaxCurrent indicates an expected call of SetMaxCurrent.
|
||||||
|
func (mr *MockCircuitMockRecorder) SetMaxCurrent(arg0 any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMaxCurrent", reflect.TypeOf((*MockCircuit)(nil).SetMaxCurrent), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMaxPower mocks base method.
|
||||||
|
func (m *MockCircuit) SetMaxPower(arg0 float64) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
m.ctrl.Call(m, "SetMaxPower", arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMaxPower indicates an expected call of SetMaxPower.
|
||||||
|
func (mr *MockCircuitMockRecorder) SetMaxPower(arg0 any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMaxPower", reflect.TypeOf((*MockCircuit)(nil).SetMaxPower), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTitle mocks base method.
|
||||||
|
func (m *MockCircuit) SetTitle(arg0 string) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
m.ctrl.Call(m, "SetTitle", arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTitle indicates an expected call of SetTitle.
|
||||||
|
func (mr *MockCircuitMockRecorder) SetTitle(arg0 any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTitle", reflect.TypeOf((*MockCircuit)(nil).SetTitle), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update mocks base method.
|
||||||
|
func (m *MockCircuit) Update(arg0 []CircuitLoad) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Update", arg0)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update indicates an expected call of Update.
|
||||||
|
func (mr *MockCircuitMockRecorder) Update(arg0 any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockCircuit)(nil).Update), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateCurrent mocks base method.
|
||||||
|
func (m *MockCircuit) ValidateCurrent(arg0, arg1 float64) float64 {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "ValidateCurrent", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].(float64)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateCurrent indicates an expected call of ValidateCurrent.
|
||||||
|
func (mr *MockCircuitMockRecorder) ValidateCurrent(arg0, arg1 any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateCurrent", reflect.TypeOf((*MockCircuit)(nil).ValidateCurrent), arg0, arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePower mocks base method.
|
||||||
|
func (m *MockCircuit) ValidatePower(arg0, arg1 float64) float64 {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "ValidatePower", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].(float64)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePower indicates an expected call of ValidatePower.
|
||||||
|
func (mr *MockCircuitMockRecorder) ValidatePower(arg0, arg1 any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidatePower", reflect.TypeOf((*MockCircuit)(nil).ValidatePower), arg0, arg1)
|
||||||
|
}
|
||||||
|
|
|
||||||
164
cmd/setup.go
164
cmd/setup.go
|
|
@ -45,6 +45,7 @@ import (
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/libp2p/zeroconf/v2"
|
"github.com/libp2p/zeroconf/v2"
|
||||||
|
"github.com/spf13/cast"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
@ -95,6 +96,7 @@ type globalConfig struct {
|
||||||
Tariffs tariffConfig
|
Tariffs tariffConfig
|
||||||
Site map[string]interface{}
|
Site map[string]interface{}
|
||||||
Loadpoints []map[string]interface{}
|
Loadpoints []map[string]interface{}
|
||||||
|
Circuits []config.Named
|
||||||
}
|
}
|
||||||
|
|
||||||
type mqttConfig struct {
|
type mqttConfig struct {
|
||||||
|
|
@ -183,6 +185,115 @@ func loadConfigFile(conf *globalConfig) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func configureCircuits(static []config.Named, names ...string) error {
|
||||||
|
children := slices.Clone(static)
|
||||||
|
|
||||||
|
// TODO: check for circular references
|
||||||
|
NEXT:
|
||||||
|
for i, cc := range children {
|
||||||
|
if cc.Name == "" {
|
||||||
|
return fmt.Errorf("cannot create circuit: missing name")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := nameValid(cc.Name); err != nil {
|
||||||
|
return fmt.Errorf("cannot create circuit: duplicate name: %s", cc.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parent := cast.ToString(cc.Property("parent")); parent != "" {
|
||||||
|
if _, err := config.Circuits().ByName(parent); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log := util.NewLogger("circuit-" + cc.Name)
|
||||||
|
instance, err := core.NewCircuitFromConfig(log, cc.Other)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot create circuit '%s': %w", cc.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure config has title
|
||||||
|
if instance.GetTitle() == "" {
|
||||||
|
//lint:ignore SA1019 as Title is safe on ascii
|
||||||
|
instance.SetTitle(strings.Title(cc.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := config.Circuits().Add(config.NewStaticDevice(cc, instance)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
children = slices.Delete(children, i, i+1)
|
||||||
|
goto NEXT
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(children) > 0 {
|
||||||
|
return fmt.Errorf("circuit is missing parent: %s", children[0].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// append devices from database
|
||||||
|
configurable, err := config.ConfigurationsByClass(templates.Circuit)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
children2 := slices.Clone(configurable)
|
||||||
|
|
||||||
|
NEXT2:
|
||||||
|
for i, conf := range children2 {
|
||||||
|
cc := conf.Named()
|
||||||
|
|
||||||
|
if len(names) > 0 && !slices.Contains(names, cc.Name) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if parent := cast.ToString(cc.Property("parent")); parent != "" {
|
||||||
|
if _, err := config.Circuits().ByName(parent); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log := util.NewLogger("circuit-" + cc.Name)
|
||||||
|
instance, err := core.NewCircuitFromConfig(log, cc.Other)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot create circuit '%s': %w", cc.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure config has title
|
||||||
|
if instance.GetTitle() == "" {
|
||||||
|
//lint:ignore SA1019 as Title is safe on ascii
|
||||||
|
instance.SetTitle(strings.Title(cc.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := config.Circuits().Add(config.NewConfigurableDevice(conf, instance)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
children2 = slices.Delete(children2, i, i+1)
|
||||||
|
goto NEXT2
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(children2) > 0 {
|
||||||
|
return fmt.Errorf("missing parent circuit: %s", children2[0].Named().Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rootFound bool
|
||||||
|
for _, dev := range config.Circuits().Devices() {
|
||||||
|
c := dev.Instance()
|
||||||
|
|
||||||
|
if c.GetParent() == nil {
|
||||||
|
if rootFound {
|
||||||
|
return errors.New("cannot have multiple root circuits")
|
||||||
|
}
|
||||||
|
rootFound = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !rootFound && len(config.Circuits().Devices()) > 0 {
|
||||||
|
return errors.New("root circuit required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func configureMeters(static []config.Named, names ...string) error {
|
func configureMeters(static []config.Named, names ...string) error {
|
||||||
for i, cc := range static {
|
for i, cc := range static {
|
||||||
if cc.Name == "" {
|
if cc.Name == "" {
|
||||||
|
|
@ -647,6 +758,9 @@ func configureDevices(conf globalConfig) error {
|
||||||
if err := configureChargers(conf.Chargers); err != nil {
|
if err := configureChargers(conf.Chargers); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := configureCircuits(conf.Circuits); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return configureVehicles(conf.Vehicles)
|
return configureVehicles(conf.Vehicles)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -665,7 +779,43 @@ func configureSiteAndLoadpoints(conf globalConfig) (*core.Site, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return configureSite(conf.Site, loadpoints, tariffs)
|
site, err := configureSite(conf.Site, loadpoints, tariffs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.Circuits().Devices()) > 0 {
|
||||||
|
if err := validateCircuits(site, loadpoints); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return site, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateCircuits(site site.API, loadpoints []*core.Loadpoint) error {
|
||||||
|
CONTINUE:
|
||||||
|
for _, dev := range config.Circuits().Devices() {
|
||||||
|
instance := dev.Instance()
|
||||||
|
|
||||||
|
if instance.HasMeter() || site.GetCircuit() == instance {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, lp := range loadpoints {
|
||||||
|
if lp.GetCircuit() == instance {
|
||||||
|
continue CONTINUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("circuit %s has no meter or loadpoint assigned", dev.Config().Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if site.GetCircuit() == nil {
|
||||||
|
return errors.New("site has no circuit")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func configureSite(conf map[string]interface{}, loadpoints []*core.Loadpoint, tariffs *tariff.Tariffs) (*core.Site, error) {
|
func configureSite(conf map[string]interface{}, loadpoints []*core.Loadpoint, tariffs *tariff.Tariffs) (*core.Site, error) {
|
||||||
|
|
@ -674,19 +824,25 @@ func configureSite(conf map[string]interface{}, loadpoints []*core.Loadpoint, ta
|
||||||
return nil, fmt.Errorf("failed configuring site: %w", err)
|
return nil, fmt.Errorf("failed configuring site: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(config.Circuits().Devices()) > 0 && site.GetCircuit() == nil {
|
||||||
|
return nil, errors.New("site has no circuit")
|
||||||
|
}
|
||||||
|
|
||||||
return site, nil
|
return site, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func configureLoadpoints(conf globalConfig) (loadpoints []*core.Loadpoint, err error) {
|
func configureLoadpoints(conf globalConfig) ([]*core.Loadpoint, error) {
|
||||||
if len(conf.Loadpoints) == 0 {
|
if len(conf.Loadpoints) == 0 {
|
||||||
return nil, errors.New("missing loadpoints")
|
return nil, errors.New("missing loadpoints")
|
||||||
}
|
}
|
||||||
|
|
||||||
for id, lpc := range conf.Loadpoints {
|
var loadpoints []*core.Loadpoint
|
||||||
|
|
||||||
|
for id, cfg := range conf.Loadpoints {
|
||||||
log := util.NewLoggerWithLoadpoint("lp-"+strconv.Itoa(id+1), id+1)
|
log := util.NewLoggerWithLoadpoint("lp-"+strconv.Itoa(id+1), id+1)
|
||||||
settings := &core.Settings{Key: "lp" + strconv.Itoa(id+1) + "."}
|
settings := &core.Settings{Key: "lp" + strconv.Itoa(id+1) + "."}
|
||||||
|
|
||||||
lp, err := core.NewLoadpointFromConfig(log, settings, lpc)
|
lp, err := core.NewLoadpointFromConfig(log, settings, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed configuring loadpoint: %w", err)
|
return nil, fmt.Errorf("failed configuring loadpoint: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
140
cmd/setup_circuits_test.go
Normal file
140
cmd/setup_circuits_test.go
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/evcc-io/evcc/api"
|
||||||
|
"github.com/evcc-io/evcc/core"
|
||||||
|
"github.com/evcc-io/evcc/meter"
|
||||||
|
"github.com/evcc-io/evcc/server/db"
|
||||||
|
"github.com/evcc-io/evcc/tariff"
|
||||||
|
"github.com/evcc-io/evcc/util/config"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"go.uber.org/mock/gomock"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSetupCircuits(t *testing.T) {
|
||||||
|
suite.Run(t, new(circuitsTestSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
type circuitsTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *circuitsTestSuite) SetupSuite() {
|
||||||
|
db, err := db.New("sqlite", ":memory:")
|
||||||
|
if err != nil {
|
||||||
|
suite.T().Fatal(err)
|
||||||
|
}
|
||||||
|
config.Init(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *circuitsTestSuite) SetupTest() {
|
||||||
|
config.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *circuitsTestSuite) TestCircuitConf() {
|
||||||
|
var conf globalConfig
|
||||||
|
viper.SetConfigType("yaml")
|
||||||
|
|
||||||
|
suite.Require().NoError(viper.ReadConfig(strings.NewReader(`circuits:
|
||||||
|
- name: master
|
||||||
|
maxPower: 10000
|
||||||
|
- name: slave
|
||||||
|
parent: master
|
||||||
|
maxPower: 10000
|
||||||
|
loadpoints:
|
||||||
|
- charger: test
|
||||||
|
circuit: slave
|
||||||
|
`)))
|
||||||
|
|
||||||
|
suite.Require().NoError(viper.UnmarshalExact(&conf))
|
||||||
|
|
||||||
|
suite.Require().NoError(configureCircuits(conf.Circuits))
|
||||||
|
suite.Require().Len(config.Circuits().Devices(), 2)
|
||||||
|
suite.Require().False(config.Circuits().Devices()[0].Instance().HasMeter())
|
||||||
|
|
||||||
|
// empty charger
|
||||||
|
suite.Require().NoError(config.Chargers().Add(config.NewStaticDevice(config.Named{
|
||||||
|
Name: "test",
|
||||||
|
}, api.Charger(nil))))
|
||||||
|
|
||||||
|
lps, err := configureLoadpoints(conf)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
suite.Require().Len(lps, 1)
|
||||||
|
suite.Require().NotNil(lps[0].GetCircuit())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *circuitsTestSuite) TestLoadpointMissingCircuitError() {
|
||||||
|
var conf globalConfig
|
||||||
|
viper.SetConfigType("yaml")
|
||||||
|
|
||||||
|
suite.Require().NoError(viper.ReadConfig(strings.NewReader(`
|
||||||
|
loadpoints:
|
||||||
|
- charger: test
|
||||||
|
`)))
|
||||||
|
|
||||||
|
suite.Require().NoError(viper.UnmarshalExact(&conf))
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(suite.T())
|
||||||
|
circuit := api.NewMockCircuit(ctrl)
|
||||||
|
|
||||||
|
// mock circuit
|
||||||
|
suite.Require().NoError(config.Circuits().Add(config.NewStaticDevice(config.Named{
|
||||||
|
Name: "test",
|
||||||
|
}, api.Circuit(circuit))))
|
||||||
|
|
||||||
|
// mock charger
|
||||||
|
suite.Require().NoError(config.Chargers().Add(config.NewStaticDevice(config.Named{
|
||||||
|
Name: "test",
|
||||||
|
}, api.Charger(nil))))
|
||||||
|
|
||||||
|
lps, err := configureLoadpoints(conf)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
site := core.NewSite()
|
||||||
|
circuit.EXPECT().HasMeter().Return(false)
|
||||||
|
suite.Require().Error(validateCircuits(site, lps))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *circuitsTestSuite) TestSiteMissingCircuitError() {
|
||||||
|
var conf globalConfig
|
||||||
|
viper.SetConfigType("yaml")
|
||||||
|
|
||||||
|
suite.Require().NoError(viper.ReadConfig(strings.NewReader(`
|
||||||
|
loadpoints:
|
||||||
|
- charger: test
|
||||||
|
site:
|
||||||
|
meters:
|
||||||
|
grid: grid
|
||||||
|
`)))
|
||||||
|
|
||||||
|
suite.Require().NoError(viper.UnmarshalExact(&conf))
|
||||||
|
|
||||||
|
lps := []*core.Loadpoint{
|
||||||
|
new(core.Loadpoint),
|
||||||
|
}
|
||||||
|
|
||||||
|
// mock circuit
|
||||||
|
suite.Require().NoError(config.Circuits().Add(config.NewStaticDevice(config.Named{
|
||||||
|
Name: "test",
|
||||||
|
}, api.Circuit(nil))))
|
||||||
|
|
||||||
|
// mock meter
|
||||||
|
m, _ := meter.NewConfigurable(func() (float64, error) {
|
||||||
|
return 0, nil
|
||||||
|
})
|
||||||
|
suite.Require().NoError(config.Meters().Add(config.NewStaticDevice(config.Named{
|
||||||
|
Name: "grid",
|
||||||
|
}, api.Meter(m))))
|
||||||
|
|
||||||
|
// mock charger
|
||||||
|
suite.Require().NoError(config.Chargers().Add(config.NewStaticDevice(config.Named{
|
||||||
|
Name: "test",
|
||||||
|
}, api.Charger(nil))))
|
||||||
|
|
||||||
|
_, err := configureSite(conf.Site, lps, new(tariff.Tariffs))
|
||||||
|
suite.Require().Error(err)
|
||||||
|
}
|
||||||
|
|
@ -10,15 +10,12 @@ import (
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
const sample = `
|
|
||||||
loadpoints:
|
|
||||||
- mode: off
|
|
||||||
`
|
|
||||||
|
|
||||||
func TestYamlOff(t *testing.T) {
|
func TestYamlOff(t *testing.T) {
|
||||||
var conf globalConfig
|
var conf globalConfig
|
||||||
viper.SetConfigType("yaml")
|
viper.SetConfigType("yaml")
|
||||||
if err := viper.ReadConfig(strings.NewReader(sample)); err != nil {
|
if err := viper.ReadConfig(strings.NewReader(`loadpoints:
|
||||||
|
- mode: off
|
||||||
|
`)); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
308
core/circuit.go
Normal file
308
core/circuit.go
Normal file
|
|
@ -0,0 +1,308 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/evcc-io/evcc/api"
|
||||||
|
"github.com/evcc-io/evcc/util"
|
||||||
|
"github.com/evcc-io/evcc/util/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ api.Circuit = (*Circuit)(nil)
|
||||||
|
|
||||||
|
// the circuit instances to control the load
|
||||||
|
type Circuit struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
log *util.Logger
|
||||||
|
|
||||||
|
title string
|
||||||
|
parent api.Circuit // parent circuit
|
||||||
|
children []api.Circuit // child circuits
|
||||||
|
meter api.Meter // meter to determine current power
|
||||||
|
|
||||||
|
maxCurrent float64 // max allowed current
|
||||||
|
maxPower float64 // max allowed power
|
||||||
|
|
||||||
|
current float64
|
||||||
|
power float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCircuitFromConfig creates a new Circuit
|
||||||
|
func NewCircuitFromConfig(log *util.Logger, other map[string]interface{}) (api.Circuit, error) {
|
||||||
|
var cc struct {
|
||||||
|
Title string `mapstructure:"title"` // title
|
||||||
|
ParentRef string `mapstructure:"parent"` // parent circuit reference
|
||||||
|
MeterRef string `mapstructure:"meter"` // meter reference
|
||||||
|
MaxCurrent float64 `mapstructure:"maxCurrent"` // the max allowed current
|
||||||
|
MaxPower float64 `mapstructure:"maxPower"` // the max allowed power
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := util.DecodeOther(other, &cc); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var meter api.Meter
|
||||||
|
if cc.MeterRef != "" {
|
||||||
|
dev, err := config.Meters().ByName(cc.MeterRef)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
meter = dev.Instance()
|
||||||
|
}
|
||||||
|
|
||||||
|
circuit, err := NewCircuit(log, cc.Title, cc.MaxCurrent, cc.MaxPower, meter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cc.ParentRef != "" {
|
||||||
|
dev, err := config.Circuits().ByName(cc.ParentRef)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
circuit.SetParent(dev.Instance())
|
||||||
|
}
|
||||||
|
|
||||||
|
return circuit, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCircuit creates a circuit
|
||||||
|
func NewCircuit(log *util.Logger, title string, maxCurrent, maxPower float64, meter api.Meter) (*Circuit, error) {
|
||||||
|
c := &Circuit{
|
||||||
|
log: log,
|
||||||
|
title: title,
|
||||||
|
maxCurrent: maxCurrent,
|
||||||
|
maxPower: maxPower,
|
||||||
|
meter: meter,
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxPower == 0 {
|
||||||
|
c.log.DEBUG.Printf("validation of max power disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxCurrent == 0 {
|
||||||
|
c.log.DEBUG.Printf("validation of max phase current disabled")
|
||||||
|
} else if _, ok := meter.(api.PhaseCurrents); meter != nil && !ok {
|
||||||
|
return nil, fmt.Errorf("meter does not support phase currents")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Circuit) GetTitle() string {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
return c.title
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Circuit) SetTitle(title string) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.title = title
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetParent returns the parent circuit
|
||||||
|
func (c *Circuit) GetParent() api.Circuit {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
return c.parent
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetParent set parent circuit
|
||||||
|
func (c *Circuit) SetParent(parent api.Circuit) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.parent = parent
|
||||||
|
if parent != nil {
|
||||||
|
parent.RegisterChild(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasMeter returns the max power setting
|
||||||
|
func (c *Circuit) HasMeter() bool {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
return c.meter != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMaxPower returns the max power setting
|
||||||
|
func (c *Circuit) GetMaxPower() float64 {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
return c.maxPower
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMaxPower sets the max power
|
||||||
|
func (c *Circuit) SetMaxPower(power float64) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.maxPower = power
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMaxCurrent returns the max current setting
|
||||||
|
func (c *Circuit) GetMaxCurrent() float64 {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
return c.maxCurrent
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMaxCurrent sets the max current
|
||||||
|
func (c *Circuit) SetMaxCurrent(current float64) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.maxCurrent = current
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterChild registers child circuit
|
||||||
|
func (c *Circuit) RegisterChild(child api.Circuit) {
|
||||||
|
c.children = append(c.children, child)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Circuit) updateLoadpoints(loadpoints []api.CircuitLoad) {
|
||||||
|
c.power = 0
|
||||||
|
c.current = 0
|
||||||
|
|
||||||
|
for _, lp := range loadpoints {
|
||||||
|
if lp.GetCircuit() != c {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
c.power += lp.GetChargePower()
|
||||||
|
c.current += lp.GetMaxPhaseCurrent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Circuit) updateMeters() error {
|
||||||
|
if f, err := c.meter.CurrentPower(); err == nil {
|
||||||
|
// TODO handle negative powers
|
||||||
|
c.power = f
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("circuit power: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if phaseMeter, ok := c.meter.(api.PhaseCurrents); ok {
|
||||||
|
if l1, l2, l3, err := phaseMeter.Currents(); err == nil {
|
||||||
|
// TODO handle negative currents
|
||||||
|
c.current = max(l1, l2, l3)
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("circuit currents: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Circuit) Update(loadpoints []api.CircuitLoad) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if c.maxPower != 0 && c.power > c.maxPower {
|
||||||
|
c.log.WARN.Printf("over power detected: %gW > %gW", c.power, c.maxPower)
|
||||||
|
} else {
|
||||||
|
c.log.DEBUG.Printf("power: %gW", c.power)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.maxCurrent != 0 && c.current > c.maxCurrent {
|
||||||
|
c.log.WARN.Printf("over current detected: %gA > %gA", c.current, c.maxCurrent)
|
||||||
|
} else {
|
||||||
|
c.log.DEBUG.Printf("current: %gA", c.current)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// update children depth-first
|
||||||
|
for _, ch := range c.children {
|
||||||
|
if err := ch.Update(loadpoints); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// meter available
|
||||||
|
if c.meter != nil {
|
||||||
|
return c.updateMeters()
|
||||||
|
}
|
||||||
|
|
||||||
|
// no meter available
|
||||||
|
c.updateLoadpoints(loadpoints)
|
||||||
|
for _, ch := range c.children {
|
||||||
|
c.power += ch.GetChargePower()
|
||||||
|
c.current += ch.GetMaxPhaseCurrent()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChargePower returns the actual power
|
||||||
|
func (c *Circuit) GetChargePower() float64 {
|
||||||
|
return c.power
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMaxPhaseCurrent returns the actual current
|
||||||
|
func (c *Circuit) GetMaxPhaseCurrent() float64 {
|
||||||
|
return c.current
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePower validates power request
|
||||||
|
func (c *Circuit) ValidatePower(old, new float64) float64 {
|
||||||
|
delta := max(0, new-old)
|
||||||
|
|
||||||
|
if c.maxPower != 0 {
|
||||||
|
if c.power+delta > c.maxPower {
|
||||||
|
new = max(0, c.maxPower-c.power)
|
||||||
|
c.log.DEBUG.Printf("validate power: %gW -> %gW <= %gW at %gW: capped at %gW", old, new, c.maxPower, c.power, new)
|
||||||
|
} else {
|
||||||
|
c.log.TRACE.Printf("validate power: %gW -> %gW <= %gW at %gW: ok", old, new, c.maxPower, c.power)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.parent != nil {
|
||||||
|
res := c.parent.ValidatePower(c.power, new)
|
||||||
|
if res != new {
|
||||||
|
c.log.TRACE.Printf("validate power: %gW -> %gW at %gW: capped at %gW", old, new, c.power, new)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
return new
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateCurrent validates current request
|
||||||
|
func (c *Circuit) ValidateCurrent(old, new float64) (res float64) {
|
||||||
|
delta := max(0, new-old)
|
||||||
|
|
||||||
|
if c.maxCurrent != 0 {
|
||||||
|
if c.current+delta > c.maxCurrent {
|
||||||
|
new = max(0, c.maxCurrent-c.current)
|
||||||
|
c.log.DEBUG.Printf("validate current: %gA -> %gA <= %gA at %gA: capped at %gA", old, new, c.maxCurrent, c.current, new)
|
||||||
|
} else {
|
||||||
|
c.log.TRACE.Printf("validate current: %gA -> %gA <= %gA at %gA: ok", old, new, c.maxCurrent, c.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.parent != nil {
|
||||||
|
res := c.parent.ValidateCurrent(c.current, new)
|
||||||
|
if res != new {
|
||||||
|
c.log.TRACE.Printf("validate current: %gA -> %gA at %gA: capped by parent at %gA", old, new, c.current, res)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
return new
|
||||||
|
}
|
||||||
|
|
||||||
|
// func (c *Circuit) validate(typ string, current, old, new float64, parentFunc func(o, n float64) float64) float64 {
|
||||||
|
// delta := max(0, new-old)
|
||||||
|
|
||||||
|
// if c.maxPower != 0 {
|
||||||
|
// if c.power+delta > c.maxPower {
|
||||||
|
// new = max(0, c.maxPower-c.power)
|
||||||
|
// c.log.TRACE.Printf("validate power: %g -> %g <= %g at %g: capped at %g", old, new, c.maxPower, c.power, new)
|
||||||
|
// } else {
|
||||||
|
// c.log.TRACE.Printf("validate power: %g -> %g <= %g at %g: ok", old, new, c.maxPower, c.power)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if c.parent != nil {
|
||||||
|
// return c.parent.ValidatePower(c.power, new)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return new
|
||||||
|
// }
|
||||||
123
core/circuit_test.go
Normal file
123
core/circuit_test.go
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/evcc-io/evcc/api"
|
||||||
|
"github.com/evcc-io/evcc/util"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/mock/gomock"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCircuitPower(t *testing.T) {
|
||||||
|
log := util.NewLogger("foo")
|
||||||
|
|
||||||
|
circ := func(t *testing.T, ctrl *gomock.Controller, maxP float64) (*Circuit, *api.MockMeter) {
|
||||||
|
m := api.NewMockMeter(ctrl)
|
||||||
|
c, err := NewCircuit(log, "foo", 0, maxP, m)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return c, m
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
pm, cm1, cm2 float64
|
||||||
|
req, res float64
|
||||||
|
}{
|
||||||
|
// no load
|
||||||
|
{0, 0, 0, 0, 0},
|
||||||
|
{0, 0, 0, 1, 1},
|
||||||
|
{0, 0, 0, 2, 1},
|
||||||
|
|
||||||
|
// c1 loaded
|
||||||
|
{0, 1, 0, 0, 0},
|
||||||
|
{0, 1, 0, 1, 0},
|
||||||
|
{0, 1, 0, 2, 0},
|
||||||
|
|
||||||
|
// pc loaded
|
||||||
|
{1, 0, 0, 0, 0},
|
||||||
|
{1, 0, 0, 1, 0},
|
||||||
|
{1, 0, 0, 2, 0},
|
||||||
|
} {
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
|
||||||
|
pc, pm := circ(t, ctrl, 1)
|
||||||
|
c1, cm1 := circ(t, ctrl, 1)
|
||||||
|
c2, cm2 := circ(t, ctrl, 1)
|
||||||
|
|
||||||
|
c1.SetParent(pc)
|
||||||
|
c2.SetParent(pc)
|
||||||
|
|
||||||
|
// update meters
|
||||||
|
pm.EXPECT().CurrentPower().Return(tc.pm, nil)
|
||||||
|
cm1.EXPECT().CurrentPower().Return(tc.cm1, nil)
|
||||||
|
cm2.EXPECT().CurrentPower().Return(tc.cm2, nil)
|
||||||
|
require.NoError(t, pc.Update(nil))
|
||||||
|
|
||||||
|
require.Equal(t, tc.res, c1.ValidatePower(0, tc.req))
|
||||||
|
|
||||||
|
ctrl.Finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// func TestCircuitCurrents(t *testing.T) {
|
||||||
|
// log := util.NewLogger("foo")
|
||||||
|
|
||||||
|
// type mockMeter struct {
|
||||||
|
// *api.MockMeter
|
||||||
|
// *api.MockPhaseCurrents
|
||||||
|
// }
|
||||||
|
|
||||||
|
// circ := func(t *testing.T, ctrl *gomock.Controller, maxP float64) (*Circuit, *mockMeter) {
|
||||||
|
// m := api.NewMockMeter(ctrl)
|
||||||
|
// mc := api.NewMockPhaseCurrents(ctrl)
|
||||||
|
// mm := &mockMeter{m, mc}
|
||||||
|
// c, err := NewCircuit(log, 0, maxP, mm)
|
||||||
|
// require.NoError(t, err)
|
||||||
|
// return c, mm
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for _, tc := range []struct {
|
||||||
|
// pm, cm1, cm2 float64
|
||||||
|
// req, res float64
|
||||||
|
// }{
|
||||||
|
// // no load
|
||||||
|
// {0, 0, 0, 0, 0},
|
||||||
|
// {0, 0, 0, 1, 1},
|
||||||
|
// {0, 0, 0, 2, 1},
|
||||||
|
|
||||||
|
// // c1 loaded
|
||||||
|
// {0, 1, 0, 0, 0},
|
||||||
|
// {0, 1, 0, 1, 0},
|
||||||
|
// {0, 1, 0, 2, 0},
|
||||||
|
|
||||||
|
// // pc loaded
|
||||||
|
// {1, 0, 0, 0, 0},
|
||||||
|
// {1, 0, 0, 1, 0},
|
||||||
|
// {1, 0, 0, 2, 0},
|
||||||
|
// } {
|
||||||
|
// ctrl := gomock.NewController(t)
|
||||||
|
|
||||||
|
// pc, pm := circ(t, ctrl, 1)
|
||||||
|
// c1, cm1 := circ(t, ctrl, 1)
|
||||||
|
// c2, cm2 := circ(t, ctrl, 1)
|
||||||
|
|
||||||
|
// c1.SetParent(pc)
|
||||||
|
// c2.SetParent(pc)
|
||||||
|
|
||||||
|
// // update meters
|
||||||
|
// pm.MockMeter.EXPECT().CurrentPower().Return(tc.pm, nil)
|
||||||
|
// cm1.MockMeter.EXPECT().CurrentPower().Return(tc.cm1, nil)
|
||||||
|
// cm2.MockMeter.EXPECT().CurrentPower().Return(tc.cm2, nil)
|
||||||
|
|
||||||
|
// // update meters
|
||||||
|
// pm.MockPhaseCurrents.EXPECT().Currents().Return(tc.pm, tc.pm, tc.pm, nil)
|
||||||
|
// cm1.MockPhaseCurrents.EXPECT().Currents().Return(tc.cm1, tc.cm1, tc.cm1, nil)
|
||||||
|
// cm2.MockPhaseCurrents.EXPECT().Currents().Return(tc.cm2, tc.cm2, tc.cm2, nil)
|
||||||
|
// require.NoError(t, pc.Update(nil))
|
||||||
|
|
||||||
|
// require.Equal(t, tc.res, c1.ValidatePower(0, tc.req))
|
||||||
|
// require.Equal(t, tc.res, c1.ValidateCurrent(0, tc.req))
|
||||||
|
|
||||||
|
// ctrl.Finish()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
@ -30,6 +30,14 @@ func powerToCurrent(power float64, phases int) float64 {
|
||||||
return power / (float64(phases) * Voltage)
|
return power / (float64(phases) * Voltage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// currentToPower is a helper function to convert current to sum power
|
||||||
|
func currentToPower(current float64, phases int) float64 {
|
||||||
|
if Voltage == 0 {
|
||||||
|
panic("Voltage is not set")
|
||||||
|
}
|
||||||
|
return current * float64(phases) * Voltage
|
||||||
|
}
|
||||||
|
|
||||||
// sitePower returns the available delta power that the charger might additionally consume
|
// sitePower returns the available delta power that the charger might additionally consume
|
||||||
// negative value: available power (grid export), positive value: grid import
|
// negative value: available power (grid export), positive value: grid import
|
||||||
func sitePower(log *util.Logger, maxGrid, grid, battery, residual float64) float64 {
|
func sitePower(log *util.Logger, maxGrid, grid, battery, residual float64) float64 {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ const (
|
||||||
TariffPriceHome = "tariffPriceHome"
|
TariffPriceHome = "tariffPriceHome"
|
||||||
TariffPriceLoadpoints = "tariffPriceLoadpoints"
|
TariffPriceLoadpoints = "tariffPriceLoadpoints"
|
||||||
Vehicles = "vehicles"
|
Vehicles = "vehicles"
|
||||||
|
Circuits = "circuits"
|
||||||
PasswordConfigured = "passwordConfigured"
|
PasswordConfigured = "passwordConfigured"
|
||||||
Interval = "interval"
|
Interval = "interval"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,7 @@ type Loadpoint struct {
|
||||||
|
|
||||||
Title_ string `mapstructure:"title"` // UI title
|
Title_ string `mapstructure:"title"` // UI title
|
||||||
Priority_ int `mapstructure:"priority"` // Priority
|
Priority_ int `mapstructure:"priority"` // Priority
|
||||||
|
CircuitRef string `mapstructure:"circuit"` // Circuit reference
|
||||||
ChargerRef string `mapstructure:"charger"` // Charger reference
|
ChargerRef string `mapstructure:"charger"` // Charger reference
|
||||||
VehicleRef string `mapstructure:"vehicle"` // Vehicle reference
|
VehicleRef string `mapstructure:"vehicle"` // Vehicle reference
|
||||||
MeterRef string `mapstructure:"meter"` // Charge meter reference
|
MeterRef string `mapstructure:"meter"` // Charge meter reference
|
||||||
|
|
@ -144,6 +145,7 @@ type Loadpoint struct {
|
||||||
chargeRater api.ChargeRater
|
chargeRater api.ChargeRater
|
||||||
chargedAtStartup float64 // session energy at startup
|
chargedAtStartup float64 // session energy at startup
|
||||||
|
|
||||||
|
circuit api.Circuit // Circuit
|
||||||
chargeMeter api.Meter // Charger usage meter
|
chargeMeter api.Meter // Charger usage meter
|
||||||
vehicle api.Vehicle // Currently active vehicle
|
vehicle api.Vehicle // Currently active vehicle
|
||||||
defaultVehicle api.Vehicle // Default vehicle (disables detection)
|
defaultVehicle api.Vehicle // Default vehicle (disables detection)
|
||||||
|
|
@ -203,6 +205,14 @@ func NewLoadpointFromConfig(log *util.Logger, settings *Settings, other map[stri
|
||||||
lp.Soc.Poll.Mode = pollCharging
|
lp.Soc.Poll.Mode = pollCharging
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if lp.CircuitRef != "" {
|
||||||
|
dev, err := config.Circuits().ByName(lp.CircuitRef)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
lp.circuit = dev.Instance()
|
||||||
|
}
|
||||||
|
|
||||||
if lp.MeterRef != "" {
|
if lp.MeterRef != "" {
|
||||||
dev, err := config.Meters().ByName(lp.MeterRef)
|
dev, err := config.Meters().ByName(lp.MeterRef)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -761,6 +771,17 @@ func (lp *Loadpoint) setLimit(chargeCurrent float64) error {
|
||||||
chargeCurrent = math.Trunc(chargeCurrent)
|
chargeCurrent = math.Trunc(chargeCurrent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// apply circuit limits
|
||||||
|
if lp.circuit != nil {
|
||||||
|
currentLimit := lp.circuit.ValidateCurrent(lp.chargeCurrent, chargeCurrent)
|
||||||
|
|
||||||
|
activePhases := lp.ActivePhases()
|
||||||
|
powerLimit := lp.circuit.ValidatePower(lp.chargePower, currentToPower(chargeCurrent, activePhases))
|
||||||
|
currentLimitViaPower := powerToCurrent(powerLimit, activePhases)
|
||||||
|
|
||||||
|
chargeCurrent = min(currentLimit, currentLimitViaPower)
|
||||||
|
}
|
||||||
|
|
||||||
// set current
|
// set current
|
||||||
if chargeCurrent != lp.chargeCurrent && chargeCurrent >= lp.effectiveMinCurrent() {
|
if chargeCurrent != lp.chargeCurrent && chargeCurrent >= lp.effectiveMinCurrent() {
|
||||||
var err error
|
var err error
|
||||||
|
|
@ -1293,8 +1314,8 @@ func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batter
|
||||||
return targetCurrent
|
return targetCurrent
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateChargePower updates charge meter power
|
// UpdateChargePowerAndCurrents updates charge meter power and currents for load management
|
||||||
func (lp *Loadpoint) UpdateChargePower() {
|
func (lp *Loadpoint) UpdateChargePowerAndCurrents() {
|
||||||
bo := backoff.NewExponentialBackOff()
|
bo := backoff.NewExponentialBackOff()
|
||||||
bo.MaxElapsedTime = time.Second
|
bo.MaxElapsedTime = time.Second
|
||||||
|
|
||||||
|
|
@ -1313,12 +1334,10 @@ func (lp *Loadpoint) UpdateChargePower() {
|
||||||
lp.log.WARN.Printf("charge power must not be negative: %.0f", power)
|
lp.log.WARN.Printf("charge power must not be negative: %.0f", power)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
lp.log.ERROR.Printf("charge meter: %v", err)
|
lp.log.ERROR.Printf("charge power: %v", err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateChargeCurrents uses PhaseCurrents interface to count phases with current >=1A
|
// update charge currents
|
||||||
func (lp *Loadpoint) updateChargeCurrents() {
|
|
||||||
lp.chargeCurrents = nil
|
lp.chargeCurrents = nil
|
||||||
|
|
||||||
phaseMeter, ok := lp.chargeMeter.(api.PhaseCurrents)
|
phaseMeter, ok := lp.chargeMeter.(api.PhaseCurrents)
|
||||||
|
|
@ -1326,16 +1345,28 @@ func (lp *Loadpoint) updateChargeCurrents() {
|
||||||
return // don't guess
|
return // don't guess
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := backoff.Retry(func() error {
|
||||||
i1, i2, i3, err := phaseMeter.Currents()
|
i1, i2, i3, err := phaseMeter.Currents()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lp.log.ERROR.Printf("charge meter: %v", err)
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lp.chargeCurrents = []float64{i1, i2, i3}
|
lp.chargeCurrents = []float64{i1, i2, i3}
|
||||||
lp.log.DEBUG.Printf("charge currents: %.3gA", lp.chargeCurrents)
|
lp.log.DEBUG.Printf("charge currents: %.3gA", lp.chargeCurrents)
|
||||||
lp.publish(keys.ChargeCurrents, lp.chargeCurrents)
|
lp.publish(keys.ChargeCurrents, lp.chargeCurrents)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, bo); err != nil {
|
||||||
|
lp.log.ERROR.Printf("charge currents: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// phasesFromChargeCurrents uses PhaseCurrents interface to count phases with current >=1A
|
||||||
|
func (lp *Loadpoint) phasesFromChargeCurrents() {
|
||||||
|
if lp.chargeCurrents == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if lp.charging() && lp.phaseSwitchCompleted() {
|
if lp.charging() && lp.phaseSwitchCompleted() {
|
||||||
var phases int
|
var phases int
|
||||||
for _, i := range lp.chargeCurrents {
|
for _, i := range lp.chargeCurrents {
|
||||||
|
|
@ -1555,9 +1586,9 @@ func (lp *Loadpoint) Update(sitePower float64, autoCharge, batteryBuffered, batt
|
||||||
lp.publish(keys.SmartCostActive, autoCharge)
|
lp.publish(keys.SmartCostActive, autoCharge)
|
||||||
lp.processTasks()
|
lp.processTasks()
|
||||||
|
|
||||||
// read and publish meters first- charge power has already been updated by the site
|
// read and publish meters first- charge power and currents have already been updated by the site
|
||||||
lp.updateChargeVoltages()
|
lp.updateChargeVoltages()
|
||||||
lp.updateChargeCurrents()
|
lp.phasesFromChargeCurrents()
|
||||||
|
|
||||||
lp.sessionEnergy.SetEnvironment(greenShare, effPrice, effCo2)
|
lp.sessionEnergy.SetEnvironment(greenShare, effPrice, effCo2)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,8 @@ type API interface {
|
||||||
GetChargePower() float64
|
GetChargePower() float64
|
||||||
// GetChargePowerFlexibility returns the flexible amount of current charging power
|
// GetChargePowerFlexibility returns the flexible amount of current charging power
|
||||||
GetChargePowerFlexibility() float64
|
GetChargePowerFlexibility() float64
|
||||||
|
// GetMaxPhaseCurrent returns max phase current
|
||||||
|
GetMaxPhaseCurrent() float64
|
||||||
|
|
||||||
//
|
//
|
||||||
// charge progress
|
// charge progress
|
||||||
|
|
@ -146,4 +148,7 @@ type API interface {
|
||||||
SetVehicle(vehicle api.Vehicle)
|
SetVehicle(vehicle api.Vehicle)
|
||||||
// StartVehicleDetection allows triggering vehicle detection for debugging purposes
|
// StartVehicleDetection allows triggering vehicle detection for debugging purposes
|
||||||
StartVehicleDetection()
|
StartVehicleDetection()
|
||||||
|
|
||||||
|
// GetCircuit gets the assigned circuit
|
||||||
|
GetCircuit() api.Circuit
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,20 @@ func (mr *MockAPIMockRecorder) GetChargePowerFlexibility() *gomock.Call {
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChargePowerFlexibility", reflect.TypeOf((*MockAPI)(nil).GetChargePowerFlexibility))
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChargePowerFlexibility", reflect.TypeOf((*MockAPI)(nil).GetChargePowerFlexibility))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCircuit mocks base method.
|
||||||
|
func (m *MockAPI) GetCircuit() api.Circuit {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetCircuit")
|
||||||
|
ret0, _ := ret[0].(api.Circuit)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCircuit indicates an expected call of GetCircuit.
|
||||||
|
func (mr *MockAPIMockRecorder) GetCircuit() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCircuit", reflect.TypeOf((*MockAPI)(nil).GetCircuit))
|
||||||
|
}
|
||||||
|
|
||||||
// GetDisableThreshold mocks base method.
|
// GetDisableThreshold mocks base method.
|
||||||
func (m *MockAPI) GetDisableThreshold() float64 {
|
func (m *MockAPI) GetDisableThreshold() float64 {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|
@ -208,6 +222,20 @@ func (mr *MockAPIMockRecorder) GetMaxCurrent() *gomock.Call {
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaxCurrent", reflect.TypeOf((*MockAPI)(nil).GetMaxCurrent))
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaxCurrent", reflect.TypeOf((*MockAPI)(nil).GetMaxCurrent))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetMaxPhaseCurrent mocks base method.
|
||||||
|
func (m *MockAPI) GetMaxPhaseCurrent() float64 {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetMaxPhaseCurrent")
|
||||||
|
ret0, _ := ret[0].(float64)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMaxPhaseCurrent indicates an expected call of GetMaxPhaseCurrent.
|
||||||
|
func (mr *MockAPIMockRecorder) GetMaxPhaseCurrent() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaxPhaseCurrent", reflect.TypeOf((*MockAPI)(nil).GetMaxPhaseCurrent))
|
||||||
|
}
|
||||||
|
|
||||||
// GetMinCurrent mocks base method.
|
// GetMinCurrent mocks base method.
|
||||||
func (m *MockAPI) GetMinCurrent() float64 {
|
func (m *MockAPI) GetMinCurrent() float64 {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|
|
||||||
|
|
@ -323,6 +323,16 @@ func (lp *Loadpoint) GetChargePowerFlexibility() float64 {
|
||||||
return max(0, lp.GetChargePower()-lp.EffectiveMinPower())
|
return max(0, lp.GetChargePower()-lp.EffectiveMinPower())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetMaxPhaseCurrent returns the current charge power
|
||||||
|
func (lp *Loadpoint) GetMaxPhaseCurrent() float64 {
|
||||||
|
lp.RLock()
|
||||||
|
defer lp.RUnlock()
|
||||||
|
if lp.chargeCurrents == nil {
|
||||||
|
return lp.chargeCurrent
|
||||||
|
}
|
||||||
|
return max(lp.chargeCurrents[0], lp.chargeCurrents[1], lp.chargeCurrents[2])
|
||||||
|
}
|
||||||
|
|
||||||
// GetMinCurrent returns the min loadpoint current
|
// GetMinCurrent returns the min loadpoint current
|
||||||
func (lp *Loadpoint) GetMinCurrent() float64 {
|
func (lp *Loadpoint) GetMinCurrent() float64 {
|
||||||
lp.RLock()
|
lp.RLock()
|
||||||
|
|
@ -498,3 +508,16 @@ func (lp *Loadpoint) SetSmartCostLimit(val float64) {
|
||||||
lp.publish(keys.SmartCostLimit, lp.smartCostLimit)
|
lp.publish(keys.SmartCostLimit, lp.smartCostLimit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCircuit returns the assigned circuit
|
||||||
|
func (lp *Loadpoint) GetCircuit() api.Circuit {
|
||||||
|
lp.RLock()
|
||||||
|
defer lp.RUnlock()
|
||||||
|
|
||||||
|
// return untyped nil
|
||||||
|
if lp.circuit == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return lp.circuit
|
||||||
|
}
|
||||||
|
|
|
||||||
25
core/site.go
25
core/site.go
|
|
@ -68,10 +68,13 @@ type Site struct {
|
||||||
Title string `mapstructure:"title"` // UI title
|
Title string `mapstructure:"title"` // UI title
|
||||||
Voltage float64 `mapstructure:"voltage"` // Operating voltage. 230V for Germany.
|
Voltage float64 `mapstructure:"voltage"` // Operating voltage. 230V for Germany.
|
||||||
ResidualPower float64 `mapstructure:"residualPower"` // PV meter only: household usage. Grid meter: household safety margin
|
ResidualPower float64 `mapstructure:"residualPower"` // PV meter only: household usage. Grid meter: household safety margin
|
||||||
Meters MetersConfig // Meter references
|
Meters MetersConfig `mapstructure:"meters"` // Meter references
|
||||||
|
CircuitRef string `mapstructure:"circuit"` // Circuit reference
|
||||||
|
|
||||||
MaxGridSupplyWhileBatteryCharging float64 `mapstructure:"maxGridSupplyWhileBatteryCharging"` // ignore battery charging if AC consumption is above this value
|
MaxGridSupplyWhileBatteryCharging float64 `mapstructure:"maxGridSupplyWhileBatteryCharging"` // ignore battery charging if AC consumption is above this value
|
||||||
|
|
||||||
// meters
|
// meters
|
||||||
|
circuit api.Circuit // Circuit
|
||||||
gridMeter api.Meter // Grid usage meter
|
gridMeter api.Meter // Grid usage meter
|
||||||
pvMeters []api.Meter // PV generation meters
|
pvMeters []api.Meter // PV generation meters
|
||||||
batteryMeters []api.Meter // Battery charging meters
|
batteryMeters []api.Meter // Battery charging meters
|
||||||
|
|
@ -162,6 +165,15 @@ func NewSiteFromConfig(
|
||||||
// add meters from config
|
// add meters from config
|
||||||
site.restoreMeters()
|
site.restoreMeters()
|
||||||
|
|
||||||
|
// circuit
|
||||||
|
if site.CircuitRef != "" {
|
||||||
|
dev, err := config.Circuits().ByName(site.CircuitRef)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
site.circuit = dev.Instance()
|
||||||
|
}
|
||||||
|
|
||||||
// grid meter
|
// grid meter
|
||||||
if site.Meters.GridMeterRef != "" {
|
if site.Meters.GridMeterRef != "" {
|
||||||
dev, err := config.Meters().ByName(site.Meters.GridMeterRef)
|
dev, err := config.Meters().ByName(site.Meters.GridMeterRef)
|
||||||
|
|
@ -753,12 +765,21 @@ func (site *Site) update(lp updater) {
|
||||||
// update all loadpoint's charge power
|
// update all loadpoint's charge power
|
||||||
var totalChargePower float64
|
var totalChargePower float64
|
||||||
for _, lp := range site.loadpoints {
|
for _, lp := range site.loadpoints {
|
||||||
lp.UpdateChargePower()
|
lp.UpdateChargePowerAndCurrents()
|
||||||
totalChargePower += lp.GetChargePower()
|
totalChargePower += lp.GetChargePower()
|
||||||
|
|
||||||
site.prioritizer.UpdateChargePowerFlexibility(lp)
|
site.prioritizer.UpdateChargePowerFlexibility(lp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update all circuits' power and currents
|
||||||
|
if site.circuit != nil {
|
||||||
|
if err := site.circuit.Update(site.loadpointsAsCircuitDevices()); err != nil {
|
||||||
|
site.log.ERROR.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
site.publishCircuits()
|
||||||
|
}
|
||||||
|
|
||||||
// prioritize if possible
|
// prioritize if possible
|
||||||
var flexiblePower float64
|
var flexiblePower float64
|
||||||
if lp.GetMode() == api.ModePV {
|
if lp.GetMode() == api.ModePV {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ type API interface {
|
||||||
Loadpoints() []loadpoint.API
|
Loadpoints() []loadpoint.API
|
||||||
Vehicles() Vehicles
|
Vehicles() Vehicles
|
||||||
|
|
||||||
|
// GetCircuit returns the assigned circuit
|
||||||
|
GetCircuit() api.Circuit
|
||||||
|
|
||||||
// Meta
|
// Meta
|
||||||
GetTitle() string
|
GetTitle() string
|
||||||
SetTitle(string)
|
SetTitle(string)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/evcc-io/evcc/core/site"
|
"github.com/evcc-io/evcc/core/site"
|
||||||
"github.com/evcc-io/evcc/server/db/settings"
|
"github.com/evcc-io/evcc/server/db/settings"
|
||||||
"github.com/evcc-io/evcc/util/config"
|
"github.com/evcc-io/evcc/util/config"
|
||||||
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ site.API = (*Site)(nil)
|
var _ site.API = (*Site)(nil)
|
||||||
|
|
@ -121,13 +122,14 @@ func (site *Site) SetAuxMeterRefs(ref []string) {
|
||||||
settings.SetString(keys.AuxMeters, strings.Join(filterConfigurable(ref), ","))
|
settings.SetString(keys.AuxMeters, strings.Join(filterConfigurable(ref), ","))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loadpoints returns the list loadpoints
|
// Loadpoints returns the loadpoints as api interfaces
|
||||||
func (site *Site) Loadpoints() []loadpoint.API {
|
func (site *Site) Loadpoints() []loadpoint.API {
|
||||||
res := make([]loadpoint.API, len(site.loadpoints))
|
return lo.Map(site.loadpoints, func(lp *Loadpoint, _ int) loadpoint.API { return lp })
|
||||||
for id, lp := range site.loadpoints {
|
|
||||||
res[id] = lp
|
|
||||||
}
|
}
|
||||||
return res
|
|
||||||
|
// loadpointsAsCircuitDevices returns the loadpoints as circuit devices
|
||||||
|
func (site *Site) loadpointsAsCircuitDevices() []api.CircuitLoad {
|
||||||
|
return lo.Map(site.loadpoints, func(lp *Loadpoint, _ int) api.CircuitLoad { return lp })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vehicles returns the site vehicles
|
// Vehicles returns the site vehicles
|
||||||
|
|
@ -135,6 +137,15 @@ func (site *Site) Vehicles() site.Vehicles {
|
||||||
return &vehicles{log: site.log}
|
return &vehicles{log: site.log}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCircuit returns the circuit
|
||||||
|
func (site *Site) GetCircuit() api.Circuit {
|
||||||
|
if site.circuit == nil {
|
||||||
|
// return untyped nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return site.circuit
|
||||||
|
}
|
||||||
|
|
||||||
// GetPrioritySoc returns the PrioritySoc
|
// GetPrioritySoc returns the PrioritySoc
|
||||||
func (site *Site) GetPrioritySoc() float64 {
|
func (site *Site) GetPrioritySoc() float64 {
|
||||||
site.RLock()
|
site.RLock()
|
||||||
|
|
|
||||||
38
core/site_circuits.go
Normal file
38
core/site_circuits.go
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/evcc-io/evcc/core/keys"
|
||||||
|
"github.com/evcc-io/evcc/util"
|
||||||
|
"github.com/evcc-io/evcc/util/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type circuitStruct struct {
|
||||||
|
Power float64 `json:"power"`
|
||||||
|
Current *float64 `json:"current,omitempty"`
|
||||||
|
MaxPower float64 `json:"maxPower,omitempty"`
|
||||||
|
MaxCurrent float64 `json:"maxCurrent,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// publishCircuits returns a list of circuit titles
|
||||||
|
func (site *Site) publishCircuits() {
|
||||||
|
cc := config.Circuits().Devices()
|
||||||
|
res := make(map[string]circuitStruct, len(cc))
|
||||||
|
|
||||||
|
for _, c := range cc {
|
||||||
|
instance := c.Instance()
|
||||||
|
|
||||||
|
data := circuitStruct{
|
||||||
|
Power: instance.GetChargePower(),
|
||||||
|
MaxPower: instance.GetMaxPower(),
|
||||||
|
MaxCurrent: instance.GetMaxCurrent(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if instance.GetMaxCurrent() > 0 {
|
||||||
|
data.Current = util.PtrTo(instance.GetMaxPhaseCurrent())
|
||||||
|
}
|
||||||
|
|
||||||
|
res[c.Config().Name] = data
|
||||||
|
}
|
||||||
|
|
||||||
|
site.publish(keys.Circuits, res)
|
||||||
|
}
|
||||||
43
lm.md
Normal file
43
lm.md
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
## Heute (ohne LM)
|
||||||
|
|
||||||
|
je Ladepunkt:
|
||||||
|
|
||||||
|
- Leistung Site aktualisieren
|
||||||
|
- Leistung alle Ladepunkte aktualisieren
|
||||||
|
- Gesamtbudget berechnen
|
||||||
|
- Aktuellen Ladepunkt steuern
|
||||||
|
|
||||||
|
## Lastmanagement #8427
|
||||||
|
|
||||||
|
Setup:
|
||||||
|
|
||||||
|
- alle Circuits hierarchisch dem Parent vMeter zurodnen
|
||||||
|
|
||||||
|
je Ladepunkt:
|
||||||
|
|
||||||
|
- Leistung Site aktualisieren
|
||||||
|
- Leistung aller Ladepunkte aktualisieren
|
||||||
|
- Gesamtbudget berechnen
|
||||||
|
- Aktuellen Ladepunkt steuern
|
||||||
|
- dabei Strom/Leistung durch Circuit begrenzen
|
||||||
|
- Circuit aktualisieren oder aggregierte Strom/Leistung aus vMeter verwenden
|
||||||
|
-> u.U. mehrere Circuit-Zähler auszulesen
|
||||||
|
- eigenen Strom/Leistung an LM zurück melden
|
||||||
|
|
||||||
|
## Vorschlag zur Vereinfachung der vMeter
|
||||||
|
|
||||||
|
Setup:
|
||||||
|
|
||||||
|
- ENTFÄLLT: alle Circuits hierarchisch dem Parent vMeter zuordnen
|
||||||
|
|
||||||
|
je Ladepunkt:
|
||||||
|
|
||||||
|
- Leistung Site aktualisieren
|
||||||
|
- Leistung alle Ladepunkte aktualisieren
|
||||||
|
- NEU: Ströme alle Ladepunkte aktualisieren (falls vorhanden)
|
||||||
|
- NEU: Leistung aller Circuits depth-first aktualisieren
|
||||||
|
- dafür Werte der Ladepunkte verwenden wo kein Circuit Meter vorhanden
|
||||||
|
- Gesamtbudget berechnen
|
||||||
|
- Aktuellen Ladepunkt steuern
|
||||||
|
- dabei Strom/Leistung durch Circuit begrenzen
|
||||||
|
- ENTFÄLLT: Circuit aktualisieren oder aggregierte Strom/Leistung aus vMeter verwenden
|
||||||
|
|
@ -6,8 +6,11 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/evcc-io/evcc/api"
|
||||||
"github.com/evcc-io/evcc/charger"
|
"github.com/evcc-io/evcc/charger"
|
||||||
|
"github.com/evcc-io/evcc/core"
|
||||||
"github.com/evcc-io/evcc/meter"
|
"github.com/evcc-io/evcc/meter"
|
||||||
|
"github.com/evcc-io/evcc/util"
|
||||||
"github.com/evcc-io/evcc/util/config"
|
"github.com/evcc-io/evcc/util/config"
|
||||||
"github.com/evcc-io/evcc/util/templates"
|
"github.com/evcc-io/evcc/util/templates"
|
||||||
"github.com/evcc-io/evcc/vehicle"
|
"github.com/evcc-io/evcc/vehicle"
|
||||||
|
|
@ -50,6 +53,9 @@ func devicesHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
case templates.Vehicle:
|
case templates.Vehicle:
|
||||||
res, err = devicesConfig(class, config.Vehicles())
|
res, err = devicesConfig(class, config.Vehicles())
|
||||||
|
|
||||||
|
case templates.Circuit:
|
||||||
|
res, err = devicesConfig(class, config.Circuits())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -123,6 +129,9 @@ func deviceConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
case templates.Vehicle:
|
case templates.Vehicle:
|
||||||
res, err = deviceConfig(class, id, config.Vehicles())
|
res, err = deviceConfig(class, id, config.Vehicles())
|
||||||
|
|
||||||
|
case templates.Circuit:
|
||||||
|
res, err = deviceConfig(class, id, config.Circuits())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -166,6 +175,9 @@ func deviceStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
case templates.Vehicle:
|
case templates.Vehicle:
|
||||||
instance, err = deviceStatus(name, config.Vehicles())
|
instance, err = deviceStatus(name, config.Vehicles())
|
||||||
|
|
||||||
|
case templates.Circuit:
|
||||||
|
instance, err = deviceStatus(name, config.Circuits())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -218,6 +230,11 @@ func newDeviceHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
case templates.Vehicle:
|
case templates.Vehicle:
|
||||||
conf, err = newDevice(class, req, vehicle.NewFromConfig, config.Vehicles())
|
conf, err = newDevice(class, req, vehicle.NewFromConfig, config.Vehicles())
|
||||||
|
|
||||||
|
case templates.Circuit:
|
||||||
|
conf, err = newDevice(class, req, func(_ string, other map[string]interface{}) (api.Circuit, error) {
|
||||||
|
return core.NewCircuitFromConfig(util.NewLogger("circuit"), other)
|
||||||
|
}, config.Circuits())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -284,6 +301,11 @@ func updateDeviceHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
case templates.Vehicle:
|
case templates.Vehicle:
|
||||||
err = updateDevice(id, class, req, vehicle.NewFromConfig, config.Vehicles())
|
err = updateDevice(id, class, req, vehicle.NewFromConfig, config.Vehicles())
|
||||||
|
|
||||||
|
case templates.Circuit:
|
||||||
|
err = updateDevice(id, class, req, func(_ string, other map[string]interface{}) (api.Circuit, error) {
|
||||||
|
return core.NewCircuitFromConfig(util.NewLogger("circuit"), other)
|
||||||
|
}, config.Circuits())
|
||||||
}
|
}
|
||||||
|
|
||||||
setConfigDirty()
|
setConfigDirty()
|
||||||
|
|
@ -347,6 +369,9 @@ func deleteDeviceHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
case templates.Vehicle:
|
case templates.Vehicle:
|
||||||
err = deleteDevice(id, config.Vehicles())
|
err = deleteDevice(id, config.Vehicles())
|
||||||
|
|
||||||
|
case templates.Circuit:
|
||||||
|
err = deleteDevice(id, config.Circuits())
|
||||||
}
|
}
|
||||||
|
|
||||||
setConfigDirty()
|
setConfigDirty()
|
||||||
|
|
@ -413,6 +438,9 @@ func testConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
case templates.Vehicle:
|
case templates.Vehicle:
|
||||||
instance, err = testConfig(id, class, req, vehicle.NewFromConfig, config.Vehicles())
|
instance, err = testConfig(id, class, req, vehicle.NewFromConfig, config.Vehicles())
|
||||||
|
|
||||||
|
case templates.Circuit:
|
||||||
|
err = api.ErrNotAvailable
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,22 @@ import (
|
||||||
|
|
||||||
var bus = evbus.New()
|
var bus = evbus.New()
|
||||||
|
|
||||||
var instance = struct {
|
var instance struct {
|
||||||
meters *handler[api.Meter]
|
meters *handler[api.Meter]
|
||||||
chargers *handler[api.Charger]
|
chargers *handler[api.Charger]
|
||||||
vehicles *handler[api.Vehicle]
|
vehicles *handler[api.Vehicle]
|
||||||
}{
|
circuits *handler[api.Circuit]
|
||||||
meters: &handler[api.Meter]{topic: "meter"},
|
}
|
||||||
chargers: &handler[api.Charger]{topic: "charger"},
|
|
||||||
vehicles: &handler[api.Vehicle]{topic: "vehicle"},
|
func init() {
|
||||||
|
Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Reset() {
|
||||||
|
instance.meters = &handler[api.Meter]{topic: "meter"}
|
||||||
|
instance.chargers = &handler[api.Charger]{topic: "charger"}
|
||||||
|
instance.vehicles = &handler[api.Vehicle]{topic: "vehicle"}
|
||||||
|
instance.circuits = &handler[api.Circuit]{topic: "circuit"}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Handler[T any] interface {
|
type Handler[T any] interface {
|
||||||
|
|
@ -37,6 +45,10 @@ func Vehicles() Handler[api.Vehicle] {
|
||||||
return instance.vehicles
|
return instance.vehicles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Circuits() Handler[api.Circuit] {
|
||||||
|
return instance.circuits
|
||||||
|
}
|
||||||
|
|
||||||
// Instances returns the instances of the given devices
|
// Instances returns the instances of the given devices
|
||||||
func Instances[T any](devices []Device[T]) []T {
|
func Instances[T any](devices []Device[T]) []T {
|
||||||
res := make([]T, 0, len(devices))
|
res := make([]T, 0, len(devices))
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
type Typed struct {
|
type Typed struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Other map[string]interface{} `mapstructure:",remain"`
|
Other map[string]interface{} `mapstructure:",remain"`
|
||||||
|
|
@ -10,3 +14,13 @@ type Named struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Other map[string]interface{} `mapstructure:",remain"`
|
Other map[string]interface{} `mapstructure:",remain"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Property returns the value of the named property
|
||||||
|
func (n Named) Property(key string) any {
|
||||||
|
for k, v := range n.Other {
|
||||||
|
if strings.EqualFold(k, key) {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
15
util/ptr.go
Normal file
15
util/ptr.go
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
package util
|
||||||
|
|
||||||
|
// PtrTo returns a pointer to the value passed as argument. The zero is returned as nil.
|
||||||
|
func PtrTo[T comparable](v T) *T {
|
||||||
|
var zero T
|
||||||
|
if v == zero {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
// PtrToWithZero returns a pointer to the value passed as argument, including its zero value.
|
||||||
|
func PtrToWithZero[T any](v T) *T {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
@ -9,4 +9,5 @@ const (
|
||||||
Meter
|
Meter
|
||||||
Vehicle
|
Vehicle
|
||||||
Tariff
|
Tariff
|
||||||
|
Circuit
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const _ClassName = "ChargerMeterVehicleTariff"
|
const _ClassName = "ChargerMeterVehicleTariffCircuit"
|
||||||
|
|
||||||
var _ClassIndex = [...]uint8{0, 7, 12, 19, 25}
|
var _ClassIndex = [...]uint8{0, 7, 12, 19, 25, 32}
|
||||||
|
|
||||||
const _ClassLowerName = "chargermetervehicletariff"
|
const _ClassLowerName = "chargermetervehicletariffcircuit"
|
||||||
|
|
||||||
func (i Class) String() string {
|
func (i Class) String() string {
|
||||||
i -= 1
|
i -= 1
|
||||||
|
|
@ -29,9 +29,10 @@ func _ClassNoOp() {
|
||||||
_ = x[Meter-(2)]
|
_ = x[Meter-(2)]
|
||||||
_ = x[Vehicle-(3)]
|
_ = x[Vehicle-(3)]
|
||||||
_ = x[Tariff-(4)]
|
_ = x[Tariff-(4)]
|
||||||
|
_ = x[Circuit-(5)]
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ClassValues = []Class{Charger, Meter, Vehicle, Tariff}
|
var _ClassValues = []Class{Charger, Meter, Vehicle, Tariff, Circuit}
|
||||||
|
|
||||||
var _ClassNameToValueMap = map[string]Class{
|
var _ClassNameToValueMap = map[string]Class{
|
||||||
_ClassName[0:7]: Charger,
|
_ClassName[0:7]: Charger,
|
||||||
|
|
@ -42,6 +43,8 @@ var _ClassNameToValueMap = map[string]Class{
|
||||||
_ClassLowerName[12:19]: Vehicle,
|
_ClassLowerName[12:19]: Vehicle,
|
||||||
_ClassName[19:25]: Tariff,
|
_ClassName[19:25]: Tariff,
|
||||||
_ClassLowerName[19:25]: Tariff,
|
_ClassLowerName[19:25]: Tariff,
|
||||||
|
_ClassName[25:32]: Circuit,
|
||||||
|
_ClassLowerName[25:32]: Circuit,
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ClassNames = []string{
|
var _ClassNames = []string{
|
||||||
|
|
@ -49,6 +52,7 @@ var _ClassNames = []string{
|
||||||
_ClassName[7:12],
|
_ClassName[7:12],
|
||||||
_ClassName[12:19],
|
_ClassName[12:19],
|
||||||
_ClassName[19:25],
|
_ClassName[19:25],
|
||||||
|
_ClassName[25:32],
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClassString retrieves an enum value from the enum constants string name.
|
// ClassString retrieves an enum value from the enum constants string name.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue