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 b9ede09f64..6d9dc3ec91 100644 --- a/meter/eebus.go +++ b/meter/eebus.go @@ -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) - -// 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 eebusReadValue[T any](scenario uint, uc eebusapi.UseCaseBaseInterface, entity spineapi.EntityRemoteInterface, update func(entity spineapi.EntityRemoteInterface) (T, error)) (T, error) { + var zero T -func (c *EEBus) maDataUpdatePower(entity spineapi.EntityRemoteInterface) { - data, err := c.mm.Power(entity) - if err != nil { - c.log.ERROR.Println("Power:", err) - return + if entity == nil || !uc.IsScenarioAvailableAtEntity(entity, scenario) { + return zero, api.ErrNotAvailable } - c.log.TRACE.Printf("Power: %.0fW", data) - c.power.Set(data) -} -func (c *EEBus) maDataUpdateEnergyConsumed(entity spineapi.EntityRemoteInterface) { - data, err := c.mm.EnergyConsumed(entity) + res, err := update(entity) if err != nil { - c.log.ERROR.Println("EnergyConsumed:", err) - return + // announced but not provided + if errors.Is(err, eebusapi.ErrDataNotAvailable) { + err = api.ErrNotAvailable + } + return zero, err } - 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) + return res, nil } -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) +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.power.Get() + return c.readValue(1, 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(2, c.mm.EnergyConsumed) } -var _ api.PhaseCurrents = (*EEBus)(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() -func (c *EEBus) Currents() (float64, float64, float64, error) { - res, err := c.currents.Get() + res, err := eebusReadValue(scenario, c.mm, c.maEntity, update) if err != nil { - return 0, 0, 0, api.ErrNotAvailable - } - if len(res) != 3 { - return 0, 0, 0, errors.New("invalid phase currents") + // announced but not provided + if errors.Is(err, eebusapi.ErrDataNotAvailable) { + err = api.ErrNotAvailable + } + return 0, 0, 0, err } - return res[0], res[1], res[2], nil -} - -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) < 1 || len(res) > 3 { + return 0, 0, 0, fmt.Errorf("invalid phases: %v", res) } - if len(res) != 3 { - return 0, 0, 0, errors.New("invalid phase voltages") + + for len(res) < 3 { + res = append(res, 0) } + 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(3, 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(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, diff --git a/meter/eebus_events.go b/meter/eebus_events.go new file mode 100644 index 0000000000..e002dd4404 --- /dev/null +++ b/meter/eebus_events.go @@ -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 + } +} 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) { 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) + } +}