diff --git a/cmd/setup.go b/cmd/setup.go index ab1baebd2e..ebc8fa0b80 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -22,7 +22,6 @@ import ( "github.com/evcc-io/evcc/cmd/shutdown" "github.com/evcc-io/evcc/core" "github.com/evcc-io/evcc/core/keys" - "github.com/evcc-io/evcc/core/site" "github.com/evcc-io/evcc/hems" "github.com/evcc-io/evcc/meter" "github.com/evcc-io/evcc/provider/golang" @@ -914,7 +913,7 @@ func configureSiteAndLoadpoints(conf *globalconfig.All) (*core.Site, error) { } if len(config.Circuits().Devices()) > 0 { - if err := validateCircuits(site, loadpoints); err != nil { + if err := validateCircuits(loadpoints); err != nil { return nil, &ClassError{ClassCircuit, err} } } @@ -922,26 +921,39 @@ func configureSiteAndLoadpoints(conf *globalconfig.All) (*core.Site, error) { return site, nil } -func validateCircuits(site site.API, loadpoints []*core.Loadpoint) error { +func validateCircuits(loadpoints []*core.Loadpoint) error { + var hasRoot bool + CONTINUE: for _, dev := range config.Circuits().Devices() { instance := dev.Instance() - if instance.HasMeter() || site.GetCircuit() == instance { - continue + isRoot := instance.GetParent() == nil + if isRoot { + if hasRoot { + return errors.New("multiple root circuits") + } + + hasRoot = true } for _, lp := range loadpoints { if lp.GetCircuit() == instance { + if isRoot { + return fmt.Errorf("root circuit must not be assigned to loadpoint %s", lp.Title()) + } + continue CONTINUE } } - return fmt.Errorf("circuit %s has no meter or loadpoint assigned", dev.Config().Name) + if !isRoot && !instance.HasMeter() { + return fmt.Errorf("circuit %s has no meter and no loadpoint assigned", dev.Config().Name) + } } - if site.GetCircuit() == nil { - return errors.New("site has no circuit") + if !hasRoot { + return errors.New("missing root circuit") } return nil @@ -957,10 +969,6 @@ 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 } diff --git a/cmd/setup_circuits_test.go b/cmd/setup_circuits_test.go index 58efe0d1c3..bba5cec49a 100644 --- a/cmd/setup_circuits_test.go +++ b/cmd/setup_circuits_test.go @@ -6,10 +6,7 @@ import ( "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/api/globalconfig" - "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/stretchr/testify/suite" "go.uber.org/mock/gomock" @@ -39,7 +36,8 @@ func (suite *circuitsTestSuite) TestCircuitConf() { var conf globalconfig.All viper.SetConfigType("yaml") - suite.Require().NoError(viper.ReadConfig(strings.NewReader(`circuits: + suite.Require().NoError(viper.ReadConfig(strings.NewReader(` +circuits: - name: master maxPower: 10000 - name: slave @@ -63,11 +61,43 @@ loadpoints: lps, err := configureLoadpoints(conf) suite.Require().NoError(err) - suite.Require().Len(lps, 1) suite.Require().NotNil(lps[0].GetCircuit()) } -func (suite *circuitsTestSuite) TestLoadpointMissingCircuitError() { +func (suite *circuitsTestSuite) TestCircuitMissingLoadpoint() { + var conf globalconfig.All + viper.SetConfigType("yaml") + + suite.Require().NoError(viper.ReadConfig(strings.NewReader(` +circuits: +- name: master +- name: slave + parent: master +loadpoints: +- charger: test +`))) + + 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) + + // circuit without device + err = validateCircuits(lps) + suite.Require().Error(err) + suite.Require().Equal("circuit slave has no meter and no loadpoint assigned", err.Error()) +} + +func (suite *circuitsTestSuite) TestMissingRootCircuit() { var conf globalconfig.All viper.SetConfigType("yaml") @@ -94,47 +124,50 @@ loadpoints: lps, err := configureLoadpoints(conf) suite.Require().NoError(err) - site := core.NewSite() - circuit.EXPECT().HasMeter().Return(false) - suite.Require().Error(validateCircuits(site, lps)) + // root circuit + circuit.EXPECT().GetParent().Return(nil) + circuit.EXPECT().HasMeter().Return(true) + suite.Require().NoError(validateCircuits(lps)) + + // no root circuit + circuit.EXPECT().GetParent().Return(circuit) + // circuit.EXPECT().HasMeter().Return(true) + err = validateCircuits(lps) + suite.Require().Error(err) + suite.Require().Equal("missing root circuit", err.Error()) } -func (suite *circuitsTestSuite) TestSiteMissingCircuitError() { +func (suite *circuitsTestSuite) TestLoadpointUsingRootCircuit() { var conf globalconfig.All viper.SetConfigType("yaml") suite.Require().NoError(viper.ReadConfig(strings.NewReader(` loadpoints: - charger: test -site: - meters: - grid: grid + circuit: root `))) suite.Require().NoError(viper.UnmarshalExact(&conf)) - lps := []*core.Loadpoint{ - new(core.Loadpoint), - } + 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(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)))) + Name: "root", + }, api.Circuit(circuit)))) // 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)) + lps, err := configureLoadpoints(conf) + suite.Require().NoError(err) + + // root circuit + circuit.EXPECT().GetParent().Return(nil) + err = validateCircuits(lps) suite.Require().Error(err) + suite.Require().Equal("root circuit must not be assigned to loadpoint ", err.Error()) } diff --git a/core/circuit/config.go b/core/circuit/config.go new file mode 100644 index 0000000000..30af21a749 --- /dev/null +++ b/core/circuit/config.go @@ -0,0 +1,15 @@ +package circuit + +import ( + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util/config" +) + +func Root() api.Circuit { + for _, dev := range config.Circuits().Devices() { + if c := dev.Instance(); c.GetParent() == nil { + return c + } + } + return nil +} diff --git a/core/site.go b/core/site.go index 569f498879..bad0f92104 100644 --- a/core/site.go +++ b/core/site.go @@ -13,6 +13,7 @@ import ( "github.com/cenkalti/backoff/v4" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/cmd/shutdown" + "github.com/evcc-io/evcc/core/circuit" "github.com/evcc-io/evcc/core/coordinator" "github.com/evcc-io/evcc/core/keys" "github.com/evcc-io/evcc/core/loadpoint" @@ -69,7 +70,8 @@ type Site struct { 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 + // TODO deprecated + CircuitRef_ string `mapstructure:"circuit"` // Circuit reference MaxGridSupplyWhileBatteryCharging float64 `mapstructure:"maxGridSupplyWhileBatteryCharging"` // ignore battery charging if AC consumption is above this value @@ -169,12 +171,8 @@ func (site *Site) Boot(log *util.Logger, loadpoints []*Loadpoint, tariffs *tarif } // circuit - if site.CircuitRef != "" { - dev, err := config.Circuits().ByName(site.CircuitRef) - if err != nil { - return err - } - site.circuit = dev.Instance() + if c := circuit.Root(); c != nil { + site.circuit = c } // grid meter