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"
|
||||
)
|
||||
|
||||
//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
|
||||
type Meter interface {
|
||||
|
|
@ -202,3 +202,32 @@ type FeatureDescriber interface {
|
|||
type CsvWriter interface {
|
||||
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.
|
||||
// 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:
|
||||
//
|
||||
// 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.
|
||||
|
|
@ -324,6 +324,46 @@ func (mr *MockMeterEnergyMockRecorder) TotalEnergy() *gomock.Call {
|
|||
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.
|
||||
type MockVehicle struct {
|
||||
ctrl *gomock.Controller
|
||||
|
|
@ -636,3 +676,214 @@ func (mr *MockBatteryControllerMockRecorder) SetBatteryMode(arg0 any) *gomock.Ca
|
|||
mr.mock.ctrl.T.Helper()
|
||||
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/mux"
|
||||
"github.com/libp2p/zeroconf/v2"
|
||||
"github.com/spf13/cast"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
|
@ -95,6 +96,7 @@ type globalConfig struct {
|
|||
Tariffs tariffConfig
|
||||
Site map[string]interface{}
|
||||
Loadpoints []map[string]interface{}
|
||||
Circuits []config.Named
|
||||
}
|
||||
|
||||
type mqttConfig struct {
|
||||
|
|
@ -183,6 +185,115 @@ func loadConfigFile(conf *globalConfig) error {
|
|||
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 {
|
||||
for i, cc := range static {
|
||||
if cc.Name == "" {
|
||||
|
|
@ -647,6 +758,9 @@ func configureDevices(conf globalConfig) error {
|
|||
if err := configureChargers(conf.Chargers); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := configureCircuits(conf.Circuits); err != nil {
|
||||
return err
|
||||
}
|
||||
return configureVehicles(conf.Vehicles)
|
||||
}
|
||||
|
||||
|
|
@ -665,7 +779,43 @@ func configureSiteAndLoadpoints(conf globalConfig) (*core.Site, error) {
|
|||
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) {
|
||||
|
|
@ -674,19 +824,25 @@ func configureSite(conf map[string]interface{}, loadpoints []*core.Loadpoint, ta
|
|||
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
|
||||
}
|
||||
|
||||
func configureLoadpoints(conf globalConfig) (loadpoints []*core.Loadpoint, err error) {
|
||||
func configureLoadpoints(conf globalConfig) ([]*core.Loadpoint, error) {
|
||||
if len(conf.Loadpoints) == 0 {
|
||||
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)
|
||||
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 {
|
||||
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"
|
||||
)
|
||||
|
||||
const sample = `
|
||||
loadpoints:
|
||||
- mode: off
|
||||
`
|
||||
|
||||
func TestYamlOff(t *testing.T) {
|
||||
var conf globalConfig
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
// negative value: available power (grid export), positive value: grid import
|
||||
func sitePower(log *util.Logger, maxGrid, grid, battery, residual float64) float64 {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ const (
|
|||
TariffPriceHome = "tariffPriceHome"
|
||||
TariffPriceLoadpoints = "tariffPriceLoadpoints"
|
||||
Vehicles = "vehicles"
|
||||
Circuits = "circuits"
|
||||
PasswordConfigured = "passwordConfigured"
|
||||
Interval = "interval"
|
||||
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ type Loadpoint struct {
|
|||
|
||||
Title_ string `mapstructure:"title"` // UI title
|
||||
Priority_ int `mapstructure:"priority"` // Priority
|
||||
CircuitRef string `mapstructure:"circuit"` // Circuit reference
|
||||
ChargerRef string `mapstructure:"charger"` // Charger reference
|
||||
VehicleRef string `mapstructure:"vehicle"` // Vehicle reference
|
||||
MeterRef string `mapstructure:"meter"` // Charge meter reference
|
||||
|
|
@ -144,6 +145,7 @@ type Loadpoint struct {
|
|||
chargeRater api.ChargeRater
|
||||
chargedAtStartup float64 // session energy at startup
|
||||
|
||||
circuit api.Circuit // Circuit
|
||||
chargeMeter api.Meter // Charger usage meter
|
||||
vehicle api.Vehicle // Currently active vehicle
|
||||
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
|
||||
}
|
||||
|
||||
if lp.CircuitRef != "" {
|
||||
dev, err := config.Circuits().ByName(lp.CircuitRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lp.circuit = dev.Instance()
|
||||
}
|
||||
|
||||
if lp.MeterRef != "" {
|
||||
dev, err := config.Meters().ByName(lp.MeterRef)
|
||||
if err != nil {
|
||||
|
|
@ -761,6 +771,17 @@ func (lp *Loadpoint) setLimit(chargeCurrent float64) error {
|
|||
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
|
||||
if chargeCurrent != lp.chargeCurrent && chargeCurrent >= lp.effectiveMinCurrent() {
|
||||
var err error
|
||||
|
|
@ -1293,8 +1314,8 @@ func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batter
|
|||
return targetCurrent
|
||||
}
|
||||
|
||||
// UpdateChargePower updates charge meter power
|
||||
func (lp *Loadpoint) UpdateChargePower() {
|
||||
// UpdateChargePowerAndCurrents updates charge meter power and currents for load management
|
||||
func (lp *Loadpoint) UpdateChargePowerAndCurrents() {
|
||||
bo := backoff.NewExponentialBackOff()
|
||||
bo.MaxElapsedTime = time.Second
|
||||
|
||||
|
|
@ -1313,12 +1334,10 @@ func (lp *Loadpoint) UpdateChargePower() {
|
|||
lp.log.WARN.Printf("charge power must not be negative: %.0f", power)
|
||||
}
|
||||
} 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
|
||||
func (lp *Loadpoint) updateChargeCurrents() {
|
||||
// update charge currents
|
||||
lp.chargeCurrents = nil
|
||||
|
||||
phaseMeter, ok := lp.chargeMeter.(api.PhaseCurrents)
|
||||
|
|
@ -1326,16 +1345,28 @@ func (lp *Loadpoint) updateChargeCurrents() {
|
|||
return // don't guess
|
||||
}
|
||||
|
||||
i1, i2, i3, err := phaseMeter.Currents()
|
||||
if err != nil {
|
||||
lp.log.ERROR.Printf("charge meter: %v", err)
|
||||
if err := backoff.Retry(func() error {
|
||||
i1, i2, i3, err := phaseMeter.Currents()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lp.chargeCurrents = []float64{i1, i2, i3}
|
||||
lp.log.DEBUG.Printf("charge currents: %.3gA", 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
|
||||
}
|
||||
|
||||
lp.chargeCurrents = []float64{i1, i2, i3}
|
||||
lp.log.DEBUG.Printf("charge currents: %.3gA", lp.chargeCurrents)
|
||||
lp.publish(keys.ChargeCurrents, lp.chargeCurrents)
|
||||
|
||||
if lp.charging() && lp.phaseSwitchCompleted() {
|
||||
var phases int
|
||||
for _, i := range lp.chargeCurrents {
|
||||
|
|
@ -1555,9 +1586,9 @@ func (lp *Loadpoint) Update(sitePower float64, autoCharge, batteryBuffered, batt
|
|||
lp.publish(keys.SmartCostActive, autoCharge)
|
||||
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.updateChargeCurrents()
|
||||
lp.phasesFromChargeCurrents()
|
||||
|
||||
lp.sessionEnergy.SetEnvironment(greenShare, effPrice, effCo2)
|
||||
|
||||
|
|
|
|||
|
|
@ -124,6 +124,8 @@ type API interface {
|
|||
GetChargePower() float64
|
||||
// GetChargePowerFlexibility returns the flexible amount of current charging power
|
||||
GetChargePowerFlexibility() float64
|
||||
// GetMaxPhaseCurrent returns max phase current
|
||||
GetMaxPhaseCurrent() float64
|
||||
|
||||
//
|
||||
// charge progress
|
||||
|
|
@ -146,4 +148,7 @@ type API interface {
|
|||
SetVehicle(vehicle api.Vehicle)
|
||||
// StartVehicleDetection allows triggering vehicle detection for debugging purposes
|
||||
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))
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (m *MockAPI) GetDisableThreshold() float64 {
|
||||
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))
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (m *MockAPI) GetMinCurrent() float64 {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
|
|||
|
|
@ -323,6 +323,16 @@ func (lp *Loadpoint) GetChargePowerFlexibility() float64 {
|
|||
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
|
||||
func (lp *Loadpoint) GetMinCurrent() float64 {
|
||||
lp.RLock()
|
||||
|
|
@ -498,3 +508,16 @@ func (lp *Loadpoint) SetSmartCostLimit(val float64) {
|
|||
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
|
||||
}
|
||||
|
|
|
|||
33
core/site.go
33
core/site.go
|
|
@ -65,13 +65,16 @@ type Site struct {
|
|||
log *util.Logger
|
||||
|
||||
// configuration
|
||||
Title string `mapstructure:"title"` // UI title
|
||||
Voltage float64 `mapstructure:"voltage"` // Operating voltage. 230V for Germany.
|
||||
ResidualPower float64 `mapstructure:"residualPower"` // PV meter only: household usage. Grid meter: household safety margin
|
||||
Meters MetersConfig // Meter references
|
||||
MaxGridSupplyWhileBatteryCharging float64 `mapstructure:"maxGridSupplyWhileBatteryCharging"` // ignore battery charging if AC consumption is above this value
|
||||
Title string `mapstructure:"title"` // UI title
|
||||
Voltage float64 `mapstructure:"voltage"` // Operating voltage. 230V for Germany.
|
||||
ResidualPower float64 `mapstructure:"residualPower"` // PV meter only: household usage. Grid meter: household safety margin
|
||||
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
|
||||
|
||||
// meters
|
||||
circuit api.Circuit // Circuit
|
||||
gridMeter api.Meter // Grid usage meter
|
||||
pvMeters []api.Meter // PV generation meters
|
||||
batteryMeters []api.Meter // Battery charging meters
|
||||
|
|
@ -162,6 +165,15 @@ func NewSiteFromConfig(
|
|||
// add meters from config
|
||||
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
|
||||
if 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
|
||||
var totalChargePower float64
|
||||
for _, lp := range site.loadpoints {
|
||||
lp.UpdateChargePower()
|
||||
lp.UpdateChargePowerAndCurrents()
|
||||
totalChargePower += lp.GetChargePower()
|
||||
|
||||
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
|
||||
var flexiblePower float64
|
||||
if lp.GetMode() == api.ModePV {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ type API interface {
|
|||
Loadpoints() []loadpoint.API
|
||||
Vehicles() Vehicles
|
||||
|
||||
// GetCircuit returns the assigned circuit
|
||||
GetCircuit() api.Circuit
|
||||
|
||||
// Meta
|
||||
GetTitle() string
|
||||
SetTitle(string)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/evcc-io/evcc/core/site"
|
||||
"github.com/evcc-io/evcc/server/db/settings"
|
||||
"github.com/evcc-io/evcc/util/config"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
var _ site.API = (*Site)(nil)
|
||||
|
|
@ -121,13 +122,14 @@ func (site *Site) SetAuxMeterRefs(ref []string) {
|
|||
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 {
|
||||
res := make([]loadpoint.API, len(site.loadpoints))
|
||||
for id, lp := range site.loadpoints {
|
||||
res[id] = lp
|
||||
}
|
||||
return res
|
||||
return lo.Map(site.loadpoints, func(lp *Loadpoint, _ int) loadpoint.API { return lp })
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
@ -135,6 +137,15 @@ func (site *Site) Vehicles() site.Vehicles {
|
|||
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
|
||||
func (site *Site) GetPrioritySoc() float64 {
|
||||
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"
|
||||
"strconv"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/charger"
|
||||
"github.com/evcc-io/evcc/core"
|
||||
"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/templates"
|
||||
"github.com/evcc-io/evcc/vehicle"
|
||||
|
|
@ -50,6 +53,9 @@ func devicesHandler(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
case templates.Vehicle:
|
||||
res, err = devicesConfig(class, config.Vehicles())
|
||||
|
||||
case templates.Circuit:
|
||||
res, err = devicesConfig(class, config.Circuits())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -123,6 +129,9 @@ func deviceConfigHandler(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
case templates.Vehicle:
|
||||
res, err = deviceConfig(class, id, config.Vehicles())
|
||||
|
||||
case templates.Circuit:
|
||||
res, err = deviceConfig(class, id, config.Circuits())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -166,6 +175,9 @@ func deviceStatusHandler(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
case templates.Vehicle:
|
||||
instance, err = deviceStatus(name, config.Vehicles())
|
||||
|
||||
case templates.Circuit:
|
||||
instance, err = deviceStatus(name, config.Circuits())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -218,6 +230,11 @@ func newDeviceHandler(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
case templates.Vehicle:
|
||||
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 {
|
||||
|
|
@ -284,6 +301,11 @@ func updateDeviceHandler(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
case templates.Vehicle:
|
||||
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()
|
||||
|
|
@ -347,6 +369,9 @@ func deleteDeviceHandler(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
case templates.Vehicle:
|
||||
err = deleteDevice(id, config.Vehicles())
|
||||
|
||||
case templates.Circuit:
|
||||
err = deleteDevice(id, config.Circuits())
|
||||
}
|
||||
|
||||
setConfigDirty()
|
||||
|
|
@ -413,6 +438,9 @@ func testConfigHandler(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
case templates.Vehicle:
|
||||
instance, err = testConfig(id, class, req, vehicle.NewFromConfig, config.Vehicles())
|
||||
|
||||
case templates.Circuit:
|
||||
err = api.ErrNotAvailable
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -7,14 +7,22 @@ import (
|
|||
|
||||
var bus = evbus.New()
|
||||
|
||||
var instance = struct {
|
||||
var instance struct {
|
||||
meters *handler[api.Meter]
|
||||
chargers *handler[api.Charger]
|
||||
vehicles *handler[api.Vehicle]
|
||||
}{
|
||||
meters: &handler[api.Meter]{topic: "meter"},
|
||||
chargers: &handler[api.Charger]{topic: "charger"},
|
||||
vehicles: &handler[api.Vehicle]{topic: "vehicle"},
|
||||
circuits *handler[api.Circuit]
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
@ -37,6 +45,10 @@ func Vehicles() Handler[api.Vehicle] {
|
|||
return instance.vehicles
|
||||
}
|
||||
|
||||
func Circuits() Handler[api.Circuit] {
|
||||
return instance.circuits
|
||||
}
|
||||
|
||||
// Instances returns the instances of the given devices
|
||||
func Instances[T any](devices []Device[T]) []T {
|
||||
res := make([]T, 0, len(devices))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Typed struct {
|
||||
Type string `json:"type"`
|
||||
Other map[string]interface{} `mapstructure:",remain"`
|
||||
|
|
@ -10,3 +14,13 @@ type Named struct {
|
|||
Type string `json:"type"`
|
||||
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
|
||||
Vehicle
|
||||
Tariff
|
||||
Circuit
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ import (
|
|||
"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 {
|
||||
i -= 1
|
||||
|
|
@ -29,9 +29,10 @@ func _ClassNoOp() {
|
|||
_ = x[Meter-(2)]
|
||||
_ = x[Vehicle-(3)]
|
||||
_ = 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{
|
||||
_ClassName[0:7]: Charger,
|
||||
|
|
@ -42,6 +43,8 @@ var _ClassNameToValueMap = map[string]Class{
|
|||
_ClassLowerName[12:19]: Vehicle,
|
||||
_ClassName[19:25]: Tariff,
|
||||
_ClassLowerName[19:25]: Tariff,
|
||||
_ClassName[25:32]: Circuit,
|
||||
_ClassLowerName[25:32]: Circuit,
|
||||
}
|
||||
|
||||
var _ClassNames = []string{
|
||||
|
|
@ -49,6 +52,7 @@ var _ClassNames = []string{
|
|||
_ClassName[7:12],
|
||||
_ClassName[12:19],
|
||||
_ClassName[19:25],
|
||||
_ClassName[25:32],
|
||||
}
|
||||
|
||||
// ClassString retrieves an enum value from the enum constants string name.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue