From 34e1ec4b89c1fe6027595da643c4a0fbc24e4e4b Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 6 Jan 2026 12:13:20 +0100 Subject: [PATCH 01/15] EEBus meter: fix monitoring of power consumption --- meter/eebus.go | 162 +++++++++--------------------------------- meter/eebus_events.go | 140 ++++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+), 130 deletions(-) create mode 100644 meter/eebus_events.go diff --git a/meter/eebus.go b/meter/eebus.go index b9ede09f64..c2c3f2ebd6 100644 --- a/meter/eebus.go +++ b/meter/eebus.go @@ -9,12 +9,7 @@ import ( "sync" "time" - eebusapi "github.com/enbility/eebus-go/api" ucapi "github.com/enbility/eebus-go/usecases/api" - "github.com/enbility/eebus-go/usecases/eg/lpc" - "github.com/enbility/eebus-go/usecases/eg/lpp" - "github.com/enbility/eebus-go/usecases/ma/mgcp" - "github.com/enbility/eebus-go/usecases/ma/mpc" spineapi "github.com/enbility/spine-go/api" "github.com/enbility/spine-go/model" "github.com/evcc-io/evcc/api" @@ -43,6 +38,7 @@ type EEBus struct { mu sync.Mutex consumptionLimit ucapi.LoadLimit productionLimit ucapi.LoadLimit + maEntity spineapi.EntityRemoteInterface egLpcEntity spineapi.EntityRemoteInterface egLppEntity spineapi.EntityRemoteInterface } @@ -134,139 +130,69 @@ func NewEEBus(ctx context.Context, ski, ip string, usage *templates.Usage, timeo return c, nil } -var _ eebus.Device = (*EEBus)(nil) - -// UseCaseEvent implements the eebus.Device interface -func (c *EEBus) UseCaseEvent(_ spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event eebusapi.EventType) { - switch event { - // Monitoring Appliance - case mpc.DataUpdatePower, mgcp.DataUpdatePower: - c.maDataUpdatePower(entity) - case mpc.DataUpdateEnergyConsumed, mgcp.DataUpdateEnergyConsumed: - c.maDataUpdateEnergyConsumed(entity) - case mpc.DataUpdateCurrentsPerPhase, mgcp.DataUpdateCurrentPerPhase: - c.maDataUpdateCurrentPerPhase(entity) - case mpc.DataUpdateVoltagePerPhase, mgcp.DataUpdateVoltagePerPhase: - c.maDataUpdateVoltagePerPhase(entity) - - // Energy Guard - LPC - case lpc.UseCaseSupportUpdate: - c.egLpcUseCaseSupportUpdate(entity) - case lpc.DataUpdateLimit: - c.egLpcDataUpdateLimit(entity) - - // Energy Guard - LPP - case lpp.UseCaseSupportUpdate: - c.egLppUseCaseSupportUpdate(entity) - case lpp.DataUpdateLimit: - c.egLppDataUpdateLimit(entity) +func (c *EEBus) readValue(cache *util.Value[float64], update func(entity spineapi.EntityRemoteInterface) (float64, error)) (float64, error) { + if res, err := cache.Get(); err == nil { + return res, nil } -} -func (c *EEBus) maDataUpdatePower(entity spineapi.EntityRemoteInterface) { - data, err := c.mm.Power(entity) - if err != nil { - c.log.ERROR.Println("Power:", err) - return - } - c.log.TRACE.Printf("Power: %.0fW", data) - c.power.Set(data) -} + c.mu.Lock() + defer c.mu.Lock() -func (c *EEBus) maDataUpdateEnergyConsumed(entity spineapi.EntityRemoteInterface) { - data, err := c.mm.EnergyConsumed(entity) - if err != nil { - c.log.ERROR.Println("EnergyConsumed:", err) - return + if c.maEntity == nil { + return 0, api.ErrNotAvailable } - c.log.TRACE.Printf("EnergyConsumed: %.1fkWh", data/1000) - // Convert Wh to kWh - c.energy.Set(data / 1000) -} -func (c *EEBus) maDataUpdateCurrentPerPhase(entity spineapi.EntityRemoteInterface) { - data, err := c.mm.CurrentPerPhase(entity) - if err != nil { - c.log.ERROR.Println("CurrentPerPhase:", err) - return + res, err := update(c.maEntity) + if err == nil { + cache.Set(res) } - c.currents.Set(data) -} -func (c *EEBus) maDataUpdateVoltagePerPhase(entity spineapi.EntityRemoteInterface) { - data, err := c.mm.VoltagePerPhase(entity) - if err != nil { - c.log.ERROR.Println("VoltagePerPhase:", err) - return - } - c.voltages.Set(data) + return res, err } var _ api.Meter = (*EEBus)(nil) func (c *EEBus) CurrentPower() (float64, error) { - return c.power.Get() + return c.readValue(c.power, c.mm.Power) } var _ api.MeterEnergy = (*EEBus)(nil) func (c *EEBus) TotalEnergy() (float64, error) { - res, err := c.energy.Get() - if err != nil { - return 0, api.ErrNotAvailable - } - - return res, nil + return c.readValue(c.energy, c.mm.EnergyConsumed) } -var _ api.PhaseCurrents = (*EEBus)(nil) - -func (c *EEBus) Currents() (float64, float64, float64, error) { - res, err := c.currents.Get() - if err != nil { - return 0, 0, 0, api.ErrNotAvailable - } - if len(res) != 3 { - return 0, 0, 0, errors.New("invalid phase currents") +func (c *EEBus) readPhases(cache *util.Value[[]float64], update func(entity spineapi.EntityRemoteInterface) ([]float64, error)) (float64, float64, float64, error) { + if res, err := cache.Get(); err == nil { + return res[0], res[1], res[2], nil } - return res[0], res[1], res[2], nil -} -var _ api.PhaseVoltages = (*EEBus)(nil) + c.mu.Lock() + defer c.mu.Lock() -func (c *EEBus) Voltages() (float64, float64, float64, error) { - res, err := c.voltages.Get() - if err != nil { + if c.maEntity == nil { return 0, 0, 0, api.ErrNotAvailable } - if len(res) != 3 { - return 0, 0, 0, errors.New("invalid phase voltages") + + res, err := update(c.maEntity) + if err != nil { + return 0, 0, 0, err } + + cache.Set(res) return res[0], res[1], res[2], nil } -// -// Energy Guard - LPC -// - -func (c *EEBus) egLpcUseCaseSupportUpdate(entity spineapi.EntityRemoteInterface) { - c.mu.Lock() - defer c.mu.Unlock() +var _ api.PhaseCurrents = (*EEBus)(nil) - c.egLpcEntity = entity +func (c *EEBus) Currents() (float64, float64, float64, error) { + return c.readPhases(c.currents, c.mm.CurrentPerPhase) } -func (c *EEBus) egLpcDataUpdateLimit(entity spineapi.EntityRemoteInterface) { - limit, err := c.eg.EgLPCInterface.ConsumptionLimit(entity) - if err != nil { - c.log.ERROR.Println("EG LPC ConsumptionLimit:", err) - return - } - - c.mu.Lock() - defer c.mu.Unlock() +var _ api.PhaseVoltages = (*EEBus)(nil) - c.consumptionLimit = limit +func (c *EEBus) Voltages() (float64, float64, float64, error) { + return c.readPhases(c.voltages, c.mm.VoltagePerPhase) } var _ api.Dimmer = (*EEBus)(nil) @@ -312,30 +238,6 @@ func (c *EEBus) Dim(dim bool) error { return err } -// -// Energy Guard - LPP -// - -func (c *EEBus) egLppUseCaseSupportUpdate(entity spineapi.EntityRemoteInterface) { - c.mu.Lock() - defer c.mu.Unlock() - - c.egLppEntity = entity -} - -func (c *EEBus) egLppDataUpdateLimit(entity spineapi.EntityRemoteInterface) { - limit, err := c.eg.EgLPPInterface.ProductionLimit(entity) - if err != nil { - c.log.ERROR.Println("EG LPP ProductionLimit:", err) - return - } - - c.mu.Lock() - defer c.mu.Unlock() - - c.productionLimit = limit -} - var _ api.Curtailer = (*EEBus)(nil) // Curtailed implements the api.Curtailer interface diff --git a/meter/eebus_events.go b/meter/eebus_events.go new file mode 100644 index 0000000000..af032b12c3 --- /dev/null +++ b/meter/eebus_events.go @@ -0,0 +1,140 @@ +package meter + +import ( + eebusapi "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/usecases/eg/lpc" + "github.com/enbility/eebus-go/usecases/eg/lpp" + "github.com/enbility/eebus-go/usecases/ma/mgcp" + "github.com/enbility/eebus-go/usecases/ma/mpc" + spineapi "github.com/enbility/spine-go/api" + "github.com/evcc-io/evcc/server/eebus" +) + +var _ eebus.Device = (*EEBus)(nil) + +// UseCaseEvent implements the eebus.Device interface +func (c *EEBus) UseCaseEvent(_ spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event eebusapi.EventType) { + switch event { + // Monitoring Appliance + case mpc.UseCaseSupportUpdate, mgcp.UseCaseSupportUpdate: + c.maUseCaseSupportUpdate(entity) + case mpc.DataUpdatePower, mgcp.DataUpdatePower: + c.maDataUpdatePower(entity) + case mpc.DataUpdateEnergyConsumed, mgcp.DataUpdateEnergyConsumed: + c.maDataUpdateEnergyConsumed(entity) + case mpc.DataUpdateCurrentsPerPhase, mgcp.DataUpdateCurrentPerPhase: + c.maDataUpdateCurrentPerPhase(entity) + case mpc.DataUpdateVoltagePerPhase, mgcp.DataUpdateVoltagePerPhase: + c.maDataUpdateVoltagePerPhase(entity) + + // Energy Guard - LPC + case lpc.UseCaseSupportUpdate: + c.egLpcUseCaseSupportUpdate(entity) + case lpc.DataUpdateLimit: + c.egLpcDataUpdateLimit(entity) + + // Energy Guard - LPP + case lpp.UseCaseSupportUpdate: + c.egLppUseCaseSupportUpdate(entity) + case lpp.DataUpdateLimit: + c.egLppDataUpdateLimit(entity) + } +} + +// +// Monitoring Appliance - MPC/MGPC +// + +func (c *EEBus) maUseCaseSupportUpdate(entity spineapi.EntityRemoteInterface) { + c.mu.Lock() + defer c.mu.Lock() + + c.maEntity = entity +} + +func (c *EEBus) maDataUpdatePower(entity spineapi.EntityRemoteInterface) { + data, err := c.mm.Power(entity) + if err != nil { + c.log.ERROR.Println("Power:", err) + return + } + c.log.TRACE.Printf("Power: %.0fW", data) + c.power.Set(data) +} + +func (c *EEBus) maDataUpdateEnergyConsumed(entity spineapi.EntityRemoteInterface) { + data, err := c.mm.EnergyConsumed(entity) + if err != nil { + c.log.ERROR.Println("EnergyConsumed:", err) + return + } + c.log.TRACE.Printf("EnergyConsumed: %.1fkWh", data/1000) + // Convert Wh to kWh + c.energy.Set(data / 1000) +} + +func (c *EEBus) maDataUpdateCurrentPerPhase(entity spineapi.EntityRemoteInterface) { + data, err := c.mm.CurrentPerPhase(entity) + if err != nil { + c.log.ERROR.Println("CurrentPerPhase:", err) + return + } + c.currents.Set(data) +} + +func (c *EEBus) maDataUpdateVoltagePerPhase(entity spineapi.EntityRemoteInterface) { + data, err := c.mm.VoltagePerPhase(entity) + if err != nil { + c.log.ERROR.Println("VoltagePerPhase:", err) + return + } + c.voltages.Set(data) +} + +// +// Energy Guard - LPC +// + +func (c *EEBus) egLpcUseCaseSupportUpdate(entity spineapi.EntityRemoteInterface) { + c.mu.Lock() + defer c.mu.Unlock() + + c.egLpcEntity = entity +} + +func (c *EEBus) egLpcDataUpdateLimit(entity spineapi.EntityRemoteInterface) { + limit, err := c.eg.EgLPCInterface.ConsumptionLimit(entity) + if err != nil { + c.log.ERROR.Println("EG LPC ConsumptionLimit:", err) + return + } + + c.mu.Lock() + defer c.mu.Unlock() + + c.consumptionLimit = limit +} + +// +// Energy Guard - LPP +// + +func (c *EEBus) egLppUseCaseSupportUpdate(entity spineapi.EntityRemoteInterface) { + c.mu.Lock() + defer c.mu.Unlock() + + c.egLppEntity = entity +} + +func (c *EEBus) egLppDataUpdateLimit(entity spineapi.EntityRemoteInterface) { + limit, err := c.eg.EgLPPInterface.ProductionLimit(entity) + if err != nil { + c.log.ERROR.Println("EG LPP ProductionLimit:", err) + return + } + + c.mu.Lock() + defer c.mu.Unlock() + + c.productionLimit = limit +} From 5e6338995658640a478710270c9ec46e44dde6c6 Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 6 Jan 2026 12:14:25 +0100 Subject: [PATCH 02/15] wip --- meter/eebus.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/meter/eebus.go b/meter/eebus.go index c2c3f2ebd6..8ea14ac956 100644 --- a/meter/eebus.go +++ b/meter/eebus.go @@ -143,11 +143,12 @@ func (c *EEBus) readValue(cache *util.Value[float64], update func(entity spineap } res, err := update(c.maEntity) - if err == nil { - cache.Set(res) + if err != nil { + return 0, err } - return res, err + cache.Set(res) + return res, nil } var _ api.Meter = (*EEBus)(nil) From 7d119a7236843a37702eb75a3f0f7c7b47117805 Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 6 Jan 2026 12:20:34 +0100 Subject: [PATCH 03/15] wip --- meter/eebus.go | 4 ++-- meter/eebus_events.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/meter/eebus.go b/meter/eebus.go index 8ea14ac956..ae0fac6cdf 100644 --- a/meter/eebus.go +++ b/meter/eebus.go @@ -136,7 +136,7 @@ func (c *EEBus) readValue(cache *util.Value[float64], update func(entity spineap } c.mu.Lock() - defer c.mu.Lock() + defer c.mu.Unlock() if c.maEntity == nil { return 0, api.ErrNotAvailable @@ -169,7 +169,7 @@ func (c *EEBus) readPhases(cache *util.Value[[]float64], update func(entity spin } c.mu.Lock() - defer c.mu.Lock() + defer c.mu.Unlock() if c.maEntity == nil { return 0, 0, 0, api.ErrNotAvailable diff --git a/meter/eebus_events.go b/meter/eebus_events.go index af032b12c3..1b036509a8 100644 --- a/meter/eebus_events.go +++ b/meter/eebus_events.go @@ -47,7 +47,7 @@ func (c *EEBus) UseCaseEvent(_ spineapi.DeviceRemoteInterface, entity spineapi.E func (c *EEBus) maUseCaseSupportUpdate(entity spineapi.EntityRemoteInterface) { c.mu.Lock() - defer c.mu.Lock() + defer c.mu.Unlock() c.maEntity = entity } From cc132ab6788eabe07f33e896dc3d4daef85386d0 Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 6 Jan 2026 14:50:17 +0100 Subject: [PATCH 04/15] wip --- meter/eebus.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/meter/eebus.go b/meter/eebus.go index ae0fac6cdf..169b1d4068 100644 --- a/meter/eebus.go +++ b/meter/eebus.go @@ -180,6 +180,10 @@ func (c *EEBus) readPhases(cache *util.Value[[]float64], update func(entity spin return 0, 0, 0, err } + if len(res) != 3 { + return 0, 0, 0, fmt.Errorf("invalid phases: %v", res) + } + cache.Set(res) return res[0], res[1], res[2], nil } From 95649ce7fd87506fa174ff2d840e2f4dbb138f9c Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 6 Jan 2026 15:04:31 +0100 Subject: [PATCH 05/15] wip --- meter/eebus.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/meter/eebus.go b/meter/eebus.go index 169b1d4068..59cdc7f0ac 100644 --- a/meter/eebus.go +++ b/meter/eebus.go @@ -180,8 +180,10 @@ func (c *EEBus) readPhases(cache *util.Value[[]float64], update func(entity spin return 0, 0, 0, err } - if len(res) != 3 { - return 0, 0, 0, fmt.Errorf("invalid phases: %v", res) + if len(res) == 1 { + res = append(res, 0, 0) + } else if len(res) == 2 { + res = append(res, 0) } cache.Set(res) From f4b18f75bfb91caf811f48952d830ff4a0c3f682 Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 6 Jan 2026 15:59:47 +0100 Subject: [PATCH 06/15] wip --- meter/eebus.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/meter/eebus.go b/meter/eebus.go index 59cdc7f0ac..299b745651 100644 --- a/meter/eebus.go +++ b/meter/eebus.go @@ -184,6 +184,8 @@ func (c *EEBus) readPhases(cache *util.Value[[]float64], update func(entity spin res = append(res, 0, 0) } else if len(res) == 2 { res = append(res, 0) + } else if len(res) != 3 { + return 0, 0, 0, fmt.Errorf("invalid phases: %v", res) } cache.Set(res) From 949146a33f092a4f4bbc011ce24e0132dfd98bf0 Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 6 Jan 2026 17:39:44 +0100 Subject: [PATCH 07/15] Use "root" entity --- meter/eebus_events.go | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/meter/eebus_events.go b/meter/eebus_events.go index 1b036509a8..7cfe82105a 100644 --- a/meter/eebus_events.go +++ b/meter/eebus_events.go @@ -1,6 +1,8 @@ package meter import ( + "slices" + eebusapi "github.com/enbility/eebus-go/api" "github.com/enbility/eebus-go/usecases/eg/lpc" "github.com/enbility/eebus-go/usecases/eg/lpp" @@ -19,13 +21,13 @@ func (c *EEBus) UseCaseEvent(_ spineapi.DeviceRemoteInterface, entity spineapi.E case mpc.UseCaseSupportUpdate, mgcp.UseCaseSupportUpdate: c.maUseCaseSupportUpdate(entity) case mpc.DataUpdatePower, mgcp.DataUpdatePower: - c.maDataUpdatePower(entity) + c.maAssertEntity(entity, c.maDataUpdatePower) case mpc.DataUpdateEnergyConsumed, mgcp.DataUpdateEnergyConsumed: - c.maDataUpdateEnergyConsumed(entity) + c.maAssertEntity(entity, c.maDataUpdateEnergyConsumed) case mpc.DataUpdateCurrentsPerPhase, mgcp.DataUpdateCurrentPerPhase: - c.maDataUpdateCurrentPerPhase(entity) + c.maAssertEntity(entity, c.maDataUpdateCurrentPerPhase) case mpc.DataUpdateVoltagePerPhase, mgcp.DataUpdateVoltagePerPhase: - c.maDataUpdateVoltagePerPhase(entity) + c.maAssertEntity(entity, c.maDataUpdateVoltagePerPhase) // Energy Guard - LPC case lpc.UseCaseSupportUpdate: @@ -49,7 +51,18 @@ func (c *EEBus) maUseCaseSupportUpdate(entity spineapi.EntityRemoteInterface) { c.mu.Lock() defer c.mu.Unlock() - c.maEntity = entity + // use most specific selector + if c.maEntity == nil || len(entity.Address().Entity) < len(c.maEntity.Address().Entity) { + c.maEntity = entity + } +} + +// maAssertEntity ignores foreign updates +func (c *EEBus) maAssertEntity(entity spineapi.EntityRemoteInterface, update func(entity spineapi.EntityRemoteInterface)) { + if c.maEntity == nil || !slices.Equal(entity.Address().Entity, c.maEntity.Address().Entity) { + return + } + update(entity) } func (c *EEBus) maDataUpdatePower(entity spineapi.EntityRemoteInterface) { @@ -99,7 +112,10 @@ func (c *EEBus) egLpcUseCaseSupportUpdate(entity spineapi.EntityRemoteInterface) c.mu.Lock() defer c.mu.Unlock() - c.egLpcEntity = entity + // use most specific selector + if c.egLpcEntity == nil || len(entity.Address().Entity) < len(c.egLpcEntity.Address().Entity) { + c.egLpcEntity = entity + } } func (c *EEBus) egLpcDataUpdateLimit(entity spineapi.EntityRemoteInterface) { @@ -123,7 +139,10 @@ func (c *EEBus) egLppUseCaseSupportUpdate(entity spineapi.EntityRemoteInterface) c.mu.Lock() defer c.mu.Unlock() - c.egLppEntity = entity + // use most specific selector + if c.egLppEntity == nil || len(entity.Address().Entity) < len(c.egLppEntity.Address().Entity) { + c.egLppEntity = entity + } } func (c *EEBus) egLppDataUpdateLimit(entity spineapi.EntityRemoteInterface) { From 50ea707cefa25cd171e5f317d4328e2a980e9fc3 Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 6 Jan 2026 17:49:45 +0100 Subject: [PATCH 08/15] Validate scenarios --- meter/eebus.go | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/meter/eebus.go b/meter/eebus.go index 299b745651..7f0378c154 100644 --- a/meter/eebus.go +++ b/meter/eebus.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "slices" "strings" "sync" "time" @@ -44,6 +43,7 @@ type EEBus struct { } type measurements interface { + IsScenarioAvailableAtEntity(entity spineapi.EntityRemoteInterface, scenario uint) bool Power(entity spineapi.EntityRemoteInterface) (float64, error) EnergyConsumed(entity spineapi.EntityRemoteInterface) (float64, error) CurrentPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) @@ -130,7 +130,7 @@ func NewEEBus(ctx context.Context, ski, ip string, usage *templates.Usage, timeo return c, nil } -func (c *EEBus) readValue(cache *util.Value[float64], update func(entity spineapi.EntityRemoteInterface) (float64, error)) (float64, error) { +func (c *EEBus) readValue(scenario uint, cache *util.Value[float64], update func(entity spineapi.EntityRemoteInterface) (float64, error)) (float64, error) { if res, err := cache.Get(); err == nil { return res, nil } @@ -138,7 +138,7 @@ func (c *EEBus) readValue(cache *util.Value[float64], update func(entity spineap c.mu.Lock() defer c.mu.Unlock() - if c.maEntity == nil { + if c.maEntity == nil || !c.mm.IsScenarioAvailableAtEntity(c.maEntity, scenario) { return 0, api.ErrNotAvailable } @@ -154,16 +154,16 @@ func (c *EEBus) readValue(cache *util.Value[float64], update func(entity spineap var _ api.Meter = (*EEBus)(nil) func (c *EEBus) CurrentPower() (float64, error) { - return c.readValue(c.power, c.mm.Power) + return c.readValue(1, c.power, c.mm.Power) } var _ api.MeterEnergy = (*EEBus)(nil) func (c *EEBus) TotalEnergy() (float64, error) { - return c.readValue(c.energy, c.mm.EnergyConsumed) + return c.readValue(2, c.energy, c.mm.EnergyConsumed) } -func (c *EEBus) readPhases(cache *util.Value[[]float64], update func(entity spineapi.EntityRemoteInterface) ([]float64, error)) (float64, float64, float64, error) { +func (c *EEBus) readPhases(scenario uint, cache *util.Value[[]float64], update func(entity spineapi.EntityRemoteInterface) ([]float64, error)) (float64, float64, float64, error) { if res, err := cache.Get(); err == nil { return res[0], res[1], res[2], nil } @@ -171,7 +171,7 @@ func (c *EEBus) readPhases(cache *util.Value[[]float64], update func(entity spin c.mu.Lock() defer c.mu.Unlock() - if c.maEntity == nil { + if c.maEntity == nil || !c.mm.IsScenarioAvailableAtEntity(c.maEntity, scenario) { return 0, 0, 0, api.ErrNotAvailable } @@ -195,13 +195,13 @@ func (c *EEBus) readPhases(cache *util.Value[[]float64], update func(entity spin var _ api.PhaseCurrents = (*EEBus)(nil) func (c *EEBus) Currents() (float64, float64, float64, error) { - return c.readPhases(c.currents, c.mm.CurrentPerPhase) + return c.readPhases(3, c.currents, c.mm.CurrentPerPhase) } var _ api.PhaseVoltages = (*EEBus)(nil) func (c *EEBus) Voltages() (float64, float64, float64, error) { - return c.readPhases(c.voltages, c.mm.VoltagePerPhase) + return c.readPhases(4, c.voltages, c.mm.VoltagePerPhase) } var _ api.Dimmer = (*EEBus)(nil) @@ -231,14 +231,10 @@ func (c *EEBus) Dim(dim bool) error { c.mu.Lock() defer c.mu.Unlock() - if c.egLpcEntity == nil { + if c.egLpcEntity == nil || !c.eg.EgLPCInterface.IsScenarioAvailableAtEntity(c.egLpcEntity, 1) { return api.ErrNotAvailable } - if !slices.Contains(c.eg.EgLPCInterface.AvailableScenariosForEntity(c.egLpcEntity), 1) { - return errors.New("eg lpc: scenario 1 not supported") - } - _, err := c.eg.EgLPCInterface.WriteConsumptionLimit(c.egLpcEntity, ucapi.LoadLimit{ Value: value, IsActive: dim, @@ -274,14 +270,10 @@ func (c *EEBus) Curtail(curtail bool) error { c.mu.Lock() defer c.mu.Unlock() - if c.egLppEntity == nil { + if c.egLppEntity == nil || !c.eg.EgLPPInterface.IsScenarioAvailableAtEntity(c.egLppEntity, 1) { return api.ErrNotAvailable } - if !slices.Contains(c.eg.EgLPPInterface.AvailableScenariosForEntity(c.egLppEntity), 1) { - return errors.New("eg lpp: scenario 1 not supported") - } - _, err := c.eg.EgLPPInterface.WriteProductionLimit(c.egLppEntity, ucapi.LoadLimit{ Value: value, IsActive: curtail, From cfdae9048d1dbf49f294197a204446642ad05d22 Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 6 Jan 2026 17:59:12 +0100 Subject: [PATCH 09/15] wip --- meter/eebus.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/meter/eebus.go b/meter/eebus.go index 7f0378c154..772fc8f770 100644 --- a/meter/eebus.go +++ b/meter/eebus.go @@ -180,14 +180,14 @@ func (c *EEBus) readPhases(scenario uint, cache *util.Value[[]float64], update f return 0, 0, 0, err } - if len(res) == 1 { - res = append(res, 0, 0) - } else if len(res) == 2 { - res = append(res, 0) - } else if len(res) != 3 { + if len(res) < 1 || len(res) > 3 { return 0, 0, 0, fmt.Errorf("invalid phases: %v", res) } + for len(res) < 3 { + res = append(res, 0) + } + cache.Set(res) return res[0], res[1], res[2], nil } From bdee1dffad3e46fc729fb87143e4591c42b3e0f3 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 7 Jan 2026 11:59:23 +0100 Subject: [PATCH 10/15] Better logging --- hems/eebus/eebus.go | 8 ++------ meter/eebus.go | 16 ++++------------ server/eebus/helper.go | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/hems/eebus/eebus.go b/hems/eebus/eebus.go index 11965fdf95..7df000920c 100644 --- a/hems/eebus/eebus.go +++ b/hems/eebus/eebus.go @@ -141,12 +141,8 @@ func NewEEBus(ctx context.Context, ski string, limits Limits, passthrough func(b } // controllable system - for _, s := range c.cs.CsLPCInterface.RemoteEntitiesScenarios() { - c.log.DEBUG.Printf("ski %s CS LPC scenarios: %v", s.Entity.Device().Ski(), s.Scenarios) - } - for _, s := range c.cs.CsLPPInterface.RemoteEntitiesScenarios() { - c.log.DEBUG.Printf("ski %s CS LPP scenarios: %v", s.Entity.Device().Ski(), s.Scenarios) - } + eebus.LogEntities(c.log.DEBUG, "CS LPC", c.cs.CsLPCInterface) + eebus.LogEntities(c.log.DEBUG, "CS LPP", c.cs.CsLPPInterface) // set initial values if err := c.cs.CsLPCInterface.SetConsumptionNominalMax(limits.ContractualConsumptionNominalMax); err != nil { diff --git a/meter/eebus.go b/meter/eebus.go index 772fc8f770..a7045e99fa 100644 --- a/meter/eebus.go +++ b/meter/eebus.go @@ -112,20 +112,12 @@ func NewEEBus(ctx context.Context, ski, ip string, usage *templates.Usage, timeo } // monitoring appliance - for _, s := range c.ma.MaMPCInterface.RemoteEntitiesScenarios() { - c.log.DEBUG.Printf("ski %s MA MPC scenarios: %v", s.Entity.Device().Ski(), s.Scenarios) - } - for _, s := range c.ma.MaMGCPInterface.RemoteEntitiesScenarios() { - c.log.DEBUG.Printf("ski %s MA MGCP scenarios: %v", s.Entity.Device().Ski(), s.Scenarios) - } + eebus.LogEntities(c.log.DEBUG, "MA MPC", c.ma.MaMPCInterface) + eebus.LogEntities(c.log.DEBUG, "MA MGCP", c.ma.MaMGCPInterface) // energy guard - for _, s := range c.eg.EgLPCInterface.RemoteEntitiesScenarios() { - c.log.DEBUG.Printf("ski %s EG LPC scenarios: %v", s.Entity.Device().Ski(), s.Scenarios) - } - for _, s := range c.eg.EgLPPInterface.RemoteEntitiesScenarios() { - c.log.DEBUG.Printf("ski %s EG LPP scenarios: %v", s.Entity.Device().Ski(), s.Scenarios) - } + eebus.LogEntities(c.log.DEBUG, "EG LPC", c.eg.EgLPCInterface) + eebus.LogEntities(c.log.DEBUG, "EG LPP", c.eg.EgLPPInterface) return c, nil } diff --git a/server/eebus/helper.go b/server/eebus/helper.go index 8aa7f61fc9..e6e5879600 100644 --- a/server/eebus/helper.go +++ b/server/eebus/helper.go @@ -2,6 +2,7 @@ package eebus import ( "errors" + "log" eebusapi "github.com/enbility/eebus-go/api" "github.com/evcc-io/evcc/api" @@ -13,3 +14,19 @@ func WrapError(err error) error { } return err } + +func LogEntities(log *log.Logger, actor string, uc eebusapi.UseCaseInterface) { + ss := uc.RemoteEntitiesScenarios() + if len(ss) > 0 { + log.Printf("%s:", actor) + } + + for _, s := range ss { + var desc string + if d := s.Entity.Description(); d != nil { + desc = string(*d) + } + + log.Printf(" entity: %s scenarios: %v meta: %s (%s)", s.Entity.Address(), s.Scenarios, s.Entity.EntityType(), desc) + } +} From 92812aa75c9fd6610f25e7f9f91ce0e5ec52cca7 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 7 Jan 2026 12:13:56 +0100 Subject: [PATCH 11/15] Trim logging --- server/eebus/eebus.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/server/eebus/eebus.go b/server/eebus/eebus.go index b54c0e6c85..3b1551aab7 100644 --- a/server/eebus/eebus.go +++ b/server/eebus/eebus.go @@ -7,6 +7,7 @@ import ( "net" "slices" "strconv" + "strings" "sync" "time" @@ -354,12 +355,20 @@ func (c *EEBus) Tracef(format string, args ...any) { c.log.TRACE.Printf(format, args...) } +func isRelevant(s string) bool { + return strings.Contains(s, "connect") || strings.Contains(s, " event ") +} + func (c *EEBus) Debug(args ...any) { - c.log.DEBUG.Println(args...) + if s := fmt.Sprint(args...); isRelevant(s) { + c.log.DEBUG.Print(s) + } } func (c *EEBus) Debugf(format string, args ...any) { - c.log.DEBUG.Printf(format, args...) + if s := fmt.Sprintf(format, args...); isRelevant(s) { + c.log.DEBUG.Print(s) + } } func (c *EEBus) Info(args ...any) { From 592d393629302a3d49f18343fe2a786183f90dff Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 7 Jan 2026 13:15:54 +0100 Subject: [PATCH 12/15] Fix data not available --- meter/eebus.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/meter/eebus.go b/meter/eebus.go index a7045e99fa..66f31f9401 100644 --- a/meter/eebus.go +++ b/meter/eebus.go @@ -8,6 +8,7 @@ import ( "sync" "time" + eebusapi "github.com/enbility/eebus-go/api" ucapi "github.com/enbility/eebus-go/usecases/api" spineapi "github.com/enbility/spine-go/api" "github.com/enbility/spine-go/model" @@ -169,6 +170,10 @@ func (c *EEBus) readPhases(scenario uint, cache *util.Value[[]float64], update f res, err := update(c.maEntity) if err != nil { + // announced but not provided + if errors.Is(err, eebusapi.ErrDataNotAvailable) { + err = api.ErrNotAvailable + } return 0, 0, 0, err } From 6cd253754d223a7eeb43c2407b9183d9547d0200 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 7 Jan 2026 13:48:31 +0100 Subject: [PATCH 13/15] Remove all events and timeouts --- meter/eebus.go | 100 +++++++++++++++++++----------------------- meter/eebus_events.go | 77 -------------------------------- 2 files changed, 46 insertions(+), 131 deletions(-) diff --git a/meter/eebus.go b/meter/eebus.go index 66f31f9401..016fda1a2a 100644 --- a/meter/eebus.go +++ b/meter/eebus.go @@ -30,21 +30,14 @@ type EEBus struct { eg *eebus.EnergyGuard mm measurements - power *util.Value[float64] - energy *util.Value[float64] - currents *util.Value[[]float64] - voltages *util.Value[[]float64] - - mu sync.Mutex - consumptionLimit ucapi.LoadLimit - productionLimit ucapi.LoadLimit - maEntity spineapi.EntityRemoteInterface - egLpcEntity spineapi.EntityRemoteInterface - egLppEntity spineapi.EntityRemoteInterface + mu sync.Mutex + maEntity spineapi.EntityRemoteInterface + egLpcEntity spineapi.EntityRemoteInterface + egLppEntity spineapi.EntityRemoteInterface } type measurements interface { - IsScenarioAvailableAtEntity(entity spineapi.EntityRemoteInterface, scenario uint) bool + eebusapi.UseCaseBaseInterface Power(entity spineapi.EntityRemoteInterface) (float64, error) EnergyConsumed(entity spineapi.EntityRemoteInterface) (float64, error) CurrentPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) @@ -57,25 +50,23 @@ func init() { // NewEEBusFromConfig creates an EEBus meter from generic config func NewEEBusFromConfig(ctx context.Context, other map[string]any) (api.Meter, error) { - cc := struct { - Ski string - Ip string - Usage *templates.Usage - Timeout time.Duration - }{ - Timeout: 10 * time.Second, + var cc struct { + Ski string + Ip string + Usage *templates.Usage + Timeout_ time.Duration `mapstructure:"timeout"` } if err := util.DecodeOther(other, &cc); err != nil { return nil, err } - return NewEEBus(ctx, cc.Ski, cc.Ip, cc.Usage, cc.Timeout) + return NewEEBus(ctx, cc.Ski, cc.Ip, cc.Usage) } // NewEEBus creates an EEBus meter // Uses MGCP only when usage="grid", otherwise uses MPC (default) -func NewEEBus(ctx context.Context, ski, ip string, usage *templates.Usage, timeout time.Duration) (api.Meter, error) { +func NewEEBus(ctx context.Context, ski, ip string, usage *templates.Usage) (api.Meter, error) { if eebus.Instance == nil { return nil, errors.New("eebus not configured") } @@ -97,10 +88,6 @@ func NewEEBus(ctx context.Context, ski, ip string, usage *templates.Usage, timeo eg: eebus.Instance.EnergyGuard(), mm: mm, Connector: eebus.NewConnector(), - power: util.NewValue[float64](timeout), - energy: util.NewValue[float64](timeout), - currents: util.NewValue[[]float64](timeout), - voltages: util.NewValue[[]float64](timeout), } if err := eebus.Instance.RegisterDevice(ski, ip, c); err != nil { @@ -123,52 +110,48 @@ func NewEEBus(ctx context.Context, ski, ip string, usage *templates.Usage, timeo return c, nil } -func (c *EEBus) readValue(scenario uint, cache *util.Value[float64], update func(entity spineapi.EntityRemoteInterface) (float64, error)) (float64, error) { - if res, err := cache.Get(); err == nil { - return res, nil - } - - c.mu.Lock() - defer c.mu.Unlock() +func eebusReadValue[T any](scenario uint, uc eebusapi.UseCaseBaseInterface, entity spineapi.EntityRemoteInterface, update func(entity spineapi.EntityRemoteInterface) (T, error)) (T, error) { + var zero T - if c.maEntity == nil || !c.mm.IsScenarioAvailableAtEntity(c.maEntity, scenario) { - return 0, api.ErrNotAvailable + if entity == nil || !uc.IsScenarioAvailableAtEntity(entity, scenario) { + return zero, api.ErrNotAvailable } - res, err := update(c.maEntity) + res, err := update(entity) if err != nil { - return 0, err + // announced but not provided + if errors.Is(err, eebusapi.ErrDataNotAvailable) { + err = api.ErrNotAvailable + } + return zero, err } - cache.Set(res) return res, nil } +func (c *EEBus) readValue(scenario uint, update func(entity spineapi.EntityRemoteInterface) (float64, error)) (float64, error) { + c.mu.Lock() + defer c.mu.Unlock() + return eebusReadValue(scenario, c.mm, c.maEntity, update) +} + var _ api.Meter = (*EEBus)(nil) func (c *EEBus) CurrentPower() (float64, error) { - return c.readValue(1, c.power, c.mm.Power) + return c.readValue(1, c.mm.Power) } var _ api.MeterEnergy = (*EEBus)(nil) func (c *EEBus) TotalEnergy() (float64, error) { - return c.readValue(2, c.energy, c.mm.EnergyConsumed) + return c.readValue(2, c.mm.EnergyConsumed) } -func (c *EEBus) readPhases(scenario uint, cache *util.Value[[]float64], update func(entity spineapi.EntityRemoteInterface) ([]float64, error)) (float64, float64, float64, error) { - if res, err := cache.Get(); err == nil { - return res[0], res[1], res[2], nil - } - +func (c *EEBus) readPhases(scenario uint, update func(entity spineapi.EntityRemoteInterface) ([]float64, error)) (float64, float64, float64, error) { c.mu.Lock() defer c.mu.Unlock() - if c.maEntity == nil || !c.mm.IsScenarioAvailableAtEntity(c.maEntity, scenario) { - return 0, 0, 0, api.ErrNotAvailable - } - - res, err := update(c.maEntity) + res, err := eebusReadValue(scenario, c.mm, c.maEntity, update) if err != nil { // announced but not provided if errors.Is(err, eebusapi.ErrDataNotAvailable) { @@ -185,20 +168,19 @@ func (c *EEBus) readPhases(scenario uint, cache *util.Value[[]float64], update f res = append(res, 0) } - cache.Set(res) return res[0], res[1], res[2], nil } var _ api.PhaseCurrents = (*EEBus)(nil) func (c *EEBus) Currents() (float64, float64, float64, error) { - return c.readPhases(3, c.currents, c.mm.CurrentPerPhase) + return c.readPhases(3, c.mm.CurrentPerPhase) } var _ api.PhaseVoltages = (*EEBus)(nil) func (c *EEBus) Voltages() (float64, float64, float64, error) { - return c.readPhases(4, c.voltages, c.mm.VoltagePerPhase) + return c.readPhases(4, c.mm.VoltagePerPhase) } var _ api.Dimmer = (*EEBus)(nil) @@ -208,8 +190,13 @@ func (c *EEBus) Dimmed() (bool, error) { c.mu.Lock() defer c.mu.Unlock() + limit, err := eebusReadValue(1, c.eg.EgLPCInterface, c.egLpcEntity, c.eg.EgLPCInterface.ConsumptionLimit) + if err != nil { + return false, err + } + // Check if limit is active and has a valid power value - return c.consumptionLimit.IsActive && c.consumptionLimit.Value > 0, nil + return limit.IsActive && limit.Value > 0, nil } // Dim implements the api.Dimmer interface @@ -247,8 +234,13 @@ func (c *EEBus) Curtailed() (bool, error) { c.mu.Lock() defer c.mu.Unlock() + limit, err := eebusReadValue(1, c.eg.EgLPPInterface, c.egLppEntity, c.eg.EgLPPInterface.ProductionLimit) + if err != nil { + return false, err + } + // Check if limit is active and has a valid power value - return c.productionLimit.IsActive && c.productionLimit.Value > 0, nil + return limit.IsActive && limit.Value > 0, nil } // Curtail implements the api.Curtailer interface diff --git a/meter/eebus_events.go b/meter/eebus_events.go index 7cfe82105a..07948c0318 100644 --- a/meter/eebus_events.go +++ b/meter/eebus_events.go @@ -20,26 +20,14 @@ func (c *EEBus) UseCaseEvent(_ spineapi.DeviceRemoteInterface, entity spineapi.E // Monitoring Appliance case mpc.UseCaseSupportUpdate, mgcp.UseCaseSupportUpdate: c.maUseCaseSupportUpdate(entity) - case mpc.DataUpdatePower, mgcp.DataUpdatePower: - c.maAssertEntity(entity, c.maDataUpdatePower) - case mpc.DataUpdateEnergyConsumed, mgcp.DataUpdateEnergyConsumed: - c.maAssertEntity(entity, c.maDataUpdateEnergyConsumed) - case mpc.DataUpdateCurrentsPerPhase, mgcp.DataUpdateCurrentPerPhase: - c.maAssertEntity(entity, c.maDataUpdateCurrentPerPhase) - case mpc.DataUpdateVoltagePerPhase, mgcp.DataUpdateVoltagePerPhase: - c.maAssertEntity(entity, c.maDataUpdateVoltagePerPhase) // Energy Guard - LPC case lpc.UseCaseSupportUpdate: c.egLpcUseCaseSupportUpdate(entity) - case lpc.DataUpdateLimit: - c.egLpcDataUpdateLimit(entity) // Energy Guard - LPP case lpp.UseCaseSupportUpdate: c.egLppUseCaseSupportUpdate(entity) - case lpp.DataUpdateLimit: - c.egLppDataUpdateLimit(entity) } } @@ -65,45 +53,6 @@ func (c *EEBus) maAssertEntity(entity spineapi.EntityRemoteInterface, update fun update(entity) } -func (c *EEBus) maDataUpdatePower(entity spineapi.EntityRemoteInterface) { - data, err := c.mm.Power(entity) - if err != nil { - c.log.ERROR.Println("Power:", err) - return - } - c.log.TRACE.Printf("Power: %.0fW", data) - c.power.Set(data) -} - -func (c *EEBus) maDataUpdateEnergyConsumed(entity spineapi.EntityRemoteInterface) { - data, err := c.mm.EnergyConsumed(entity) - if err != nil { - c.log.ERROR.Println("EnergyConsumed:", err) - return - } - c.log.TRACE.Printf("EnergyConsumed: %.1fkWh", data/1000) - // Convert Wh to kWh - c.energy.Set(data / 1000) -} - -func (c *EEBus) maDataUpdateCurrentPerPhase(entity spineapi.EntityRemoteInterface) { - data, err := c.mm.CurrentPerPhase(entity) - if err != nil { - c.log.ERROR.Println("CurrentPerPhase:", err) - return - } - c.currents.Set(data) -} - -func (c *EEBus) maDataUpdateVoltagePerPhase(entity spineapi.EntityRemoteInterface) { - data, err := c.mm.VoltagePerPhase(entity) - if err != nil { - c.log.ERROR.Println("VoltagePerPhase:", err) - return - } - c.voltages.Set(data) -} - // // Energy Guard - LPC // @@ -118,19 +67,6 @@ func (c *EEBus) egLpcUseCaseSupportUpdate(entity spineapi.EntityRemoteInterface) } } -func (c *EEBus) egLpcDataUpdateLimit(entity spineapi.EntityRemoteInterface) { - limit, err := c.eg.EgLPCInterface.ConsumptionLimit(entity) - if err != nil { - c.log.ERROR.Println("EG LPC ConsumptionLimit:", err) - return - } - - c.mu.Lock() - defer c.mu.Unlock() - - c.consumptionLimit = limit -} - // // Energy Guard - LPP // @@ -144,16 +80,3 @@ func (c *EEBus) egLppUseCaseSupportUpdate(entity spineapi.EntityRemoteInterface) c.egLppEntity = entity } } - -func (c *EEBus) egLppDataUpdateLimit(entity spineapi.EntityRemoteInterface) { - limit, err := c.eg.EgLPPInterface.ProductionLimit(entity) - if err != nil { - c.log.ERROR.Println("EG LPP ProductionLimit:", err) - return - } - - c.mu.Lock() - defer c.mu.Unlock() - - c.productionLimit = limit -} From 97b80a8bb74a03dbfaab471de42849530b2ec40f Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 7 Jan 2026 13:57:27 +0100 Subject: [PATCH 14/15] wip --- meter/eebus_events.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/meter/eebus_events.go b/meter/eebus_events.go index 07948c0318..e002dd4404 100644 --- a/meter/eebus_events.go +++ b/meter/eebus_events.go @@ -1,8 +1,6 @@ package meter import ( - "slices" - eebusapi "github.com/enbility/eebus-go/api" "github.com/enbility/eebus-go/usecases/eg/lpc" "github.com/enbility/eebus-go/usecases/eg/lpp" @@ -45,14 +43,6 @@ func (c *EEBus) maUseCaseSupportUpdate(entity spineapi.EntityRemoteInterface) { } } -// maAssertEntity ignores foreign updates -func (c *EEBus) maAssertEntity(entity spineapi.EntityRemoteInterface, update func(entity spineapi.EntityRemoteInterface)) { - if c.maEntity == nil || !slices.Equal(entity.Address().Entity, c.maEntity.Address().Entity) { - return - } - update(entity) -} - // // Energy Guard - LPC // From b3703688510c47672e84df5384cb3c5f4b5ef5ac Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 7 Jan 2026 14:03:48 +0100 Subject: [PATCH 15/15] wip --- meter/eebus.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meter/eebus.go b/meter/eebus.go index 016fda1a2a..6d9dc3ec91 100644 --- a/meter/eebus.go +++ b/meter/eebus.go @@ -54,7 +54,7 @@ func NewEEBusFromConfig(ctx context.Context, other map[string]any) (api.Meter, e Ski string Ip string Usage *templates.Usage - Timeout_ time.Duration `mapstructure:"timeout"` + Timeout_ time.Duration `mapstructure:"timeout"` // TODO deprecated } if err := util.DecodeOther(other, &cc); err != nil {