EEBus meter: fix monitoring of power consumption (#26472)

This commit is contained in:
andig 2026-01-07 14:07:23 +01:00 • committed by GitHub
parent 4f20cc56d9
commit fea6e09b4c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 186 additions and 200 deletions

View file

@ -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 {

View file

@ -4,17 +4,12 @@ import (
"context"
"errors"
"fmt"
"slices"
"strings"
"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"
@ -35,19 +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
egLpcEntity spineapi.EntityRemoteInterface
egLppEntity spineapi.EntityRemoteInterface
mu sync.Mutex
maEntity spineapi.EntityRemoteInterface
egLpcEntity spineapi.EntityRemoteInterface
egLppEntity spineapi.EntityRemoteInterface
}
type measurements interface {
eebusapi.UseCaseBaseInterface
Power(entity spineapi.EntityRemoteInterface) (float64, error)
EnergyConsumed(entity spineapi.EntityRemoteInterface) (float64, error)
CurrentPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error)
@ -60,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"` // TODO deprecated
}
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")
}
@ -100,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 {
@ -116,157 +100,87 @@ 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
}
var _ eebus.Device = (*EEBus)(nil)
func eebusReadValue[T any](scenario uint, uc eebusapi.UseCaseBaseInterface, entity spineapi.EntityRemoteInterface, update func(entity spineapi.EntityRemoteInterface) (T, error)) (T, error) {
var zero T
// 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)
if entity == nil || !uc.IsScenarioAvailableAtEntity(entity, scenario) {
return zero, api.ErrNotAvailable
}
}
func (c *EEBus) maDataUpdatePower(entity spineapi.EntityRemoteInterface) {
data, err := c.mm.Power(entity)
res, err := update(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)
}
var _ api.Meter = (*EEBus)(nil)
func (c *EEBus) CurrentPower() (float64, error) {
return c.power.Get()
}
var _ api.MeterEnergy = (*EEBus)(nil)
func (c *EEBus) TotalEnergy() (float64, error) {
res, err := c.energy.Get()
if err != nil {
return 0, api.ErrNotAvailable
// announced but not provided
if errors.Is(err, eebusapi.ErrDataNotAvailable) {
err = api.ErrNotAvailable
}
return zero, err
}
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.mm.Power)
}
var _ api.MeterEnergy = (*EEBus)(nil)
func (c *EEBus) TotalEnergy() (float64, error) {
return c.readValue(2, c.mm.EnergyConsumed)
}
func (c *EEBus) readPhases(scenario uint, update func(entity spineapi.EntityRemoteInterface) ([]float64, error)) (float64, float64, float64, error) {
c.mu.Lock()
defer c.mu.Unlock()
res, err := eebusReadValue(scenario, c.mm, c.maEntity, update)
if err != nil {
// announced but not provided
if errors.Is(err, eebusapi.ErrDataNotAvailable) {
err = api.ErrNotAvailable
}
return 0, 0, 0, err
}
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)
}
return res[0], res[1], res[2], nil
}
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")
}
return res[0], res[1], res[2], nil
return c.readPhases(3, c.mm.CurrentPerPhase)
}
var _ api.PhaseVoltages = (*EEBus)(nil)
func (c *EEBus) Voltages() (float64, float64, float64, error) {
res, err := c.voltages.Get()
if err != nil {
return 0, 0, 0, api.ErrNotAvailable
}
if len(res) != 3 {
return 0, 0, 0, errors.New("invalid phase voltages")
}
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()
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
return c.readPhases(4, c.mm.VoltagePerPhase)
}
var _ api.Dimmer = (*EEBus)(nil)
@ -276,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
@ -296,14 +215,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,
@ -312,30 +227,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
@ -343,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
@ -363,14 +259,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,

72
meter/eebus_events.go Normal file
View file

@ -0,0 +1,72 @@
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)
// Energy Guard - LPC
case lpc.UseCaseSupportUpdate:
c.egLpcUseCaseSupportUpdate(entity)
// Energy Guard - LPP
case lpp.UseCaseSupportUpdate:
c.egLppUseCaseSupportUpdate(entity)
}
}
//
// Monitoring Appliance - MPC/MGPC
//
func (c *EEBus) maUseCaseSupportUpdate(entity spineapi.EntityRemoteInterface) {
c.mu.Lock()
defer c.mu.Unlock()
// use most specific selector
if c.maEntity == nil || len(entity.Address().Entity) < len(c.maEntity.Address().Entity) {
c.maEntity = entity
}
}
//
// Energy Guard - LPC
//
func (c *EEBus) egLpcUseCaseSupportUpdate(entity spineapi.EntityRemoteInterface) {
c.mu.Lock()
defer c.mu.Unlock()
// use most specific selector
if c.egLpcEntity == nil || len(entity.Address().Entity) < len(c.egLpcEntity.Address().Entity) {
c.egLpcEntity = entity
}
}
//
// Energy Guard - LPP
//
func (c *EEBus) egLppUseCaseSupportUpdate(entity spineapi.EntityRemoteInterface) {
c.mu.Lock()
defer c.mu.Unlock()
// use most specific selector
if c.egLppEntity == nil || len(entity.Address().Entity) < len(c.egLppEntity.Address().Entity) {
c.egLppEntity = entity
}
}

View file

@ -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) {

View file

@ -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)
}
}