From 6ef847b05b9b98960ba7c70a8b7d975ff0f6f515 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 2 Nov 2025 16:24:34 +0100 Subject: [PATCH] EEBus: add dimming (#24595) --- hems/eebus/eebus.go | 7 +++ meter/eebus.go | 114 ++++++++++++++++++++++++++++++++++++++---- server/eebus/eebus.go | 17 +++++++ 3 files changed, 129 insertions(+), 9 deletions(-) diff --git a/hems/eebus/eebus.go b/hems/eebus/eebus.go index 33fabf18f..fccbc11b4 100644 --- a/hems/eebus/eebus.go +++ b/hems/eebus/eebus.go @@ -22,6 +22,7 @@ type EEBus struct { *eebus.Connector cs *eebus.ControllableSystem ma *eebus.MonitoringAppliance + eg *eebus.EnergyGuard root api.Circuit @@ -95,6 +96,7 @@ func NewEEBus(ctx context.Context, ski string, limits Limits, root api.Circuit, root: root, cs: eebus.Instance.ControllableSystem(), ma: eebus.Instance.MonitoringAppliance(), + eg: eebus.Instance.EnergyGuard(), Connector: eebus.NewConnector(), heartbeat: util.NewValue[struct{}](2 * time.Minute), // LPC-031 interval: interval, @@ -137,6 +139,11 @@ func NewEEBus(ctx context.Context, ski string, limits Limits, root api.Circuit, c.log.DEBUG.Println("MA MGCP RemoteEntitiesScenarios:", s.Scenarios) } + // energy guard + for _, s := range c.eg.EgLPCInterface.RemoteEntitiesScenarios() { + c.log.DEBUG.Println("EG LPC RemoteEntitiesScenarios:", s.Scenarios) + } + // set initial values if err := c.cs.CsLPCInterface.SetConsumptionNominalMax(limits.ContractualConsumptionNominalMax); err != nil { c.log.ERROR.Println("CS LPC SetConsumptionNominalMax:", err) diff --git a/meter/eebus.go b/meter/eebus.go index 94c15df49..274df765b 100644 --- a/meter/eebus.go +++ b/meter/eebus.go @@ -3,32 +3,46 @@ package meter import ( "context" "errors" + "slices" + "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/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" "github.com/evcc-io/evcc/server/eebus" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/templates" ) -// EEBus is an EEBus meter implementation supporting MGCP and MPC use cases +// EEBus is an EEBus meter implementation supporting MGCP, MPC, and LPC use cases // Uses MGCP (Monitoring of Grid Connection Point) only when usage="grid" // Uses MPC (Monitoring & Power Consumption) for all other cases (default) +// Additionally supports LPC (Limitation of Power Consumption) type EEBus struct { log *util.Logger *eebus.Connector ma *eebus.MonitoringAppliance + eg *eebus.EnergyGuard mm measurements power *util.Value[float64] energy *util.Value[float64] currents *util.Value[[]float64] voltages *util.Value[[]float64] + + // TODO use util.Value + mu sync.Mutex + consumptionLimit *ucapi.LoadLimit + egLpcEntity spineapi.EntityRemoteInterface + // failsafeLimit float64 + // failsafeDuration time.Duration } type measurements interface { @@ -81,6 +95,7 @@ func NewEEBus(ctx context.Context, ski, ip string, usage *templates.Usage, timeo c := &EEBus{ log: util.NewLogger("eebus-" + useCase), ma: ma, + eg: eebus.Instance.EnergyGuard(), mm: mm, Connector: eebus.NewConnector(), power: util.NewValue[float64](timeout), @@ -108,18 +123,25 @@ func (c *EEBus) UseCaseEvent(_ spineapi.DeviceRemoteInterface, entity spineapi.E c.log.TRACE.Printf("recv: %s", event) switch event { + // Monitoring Appliance case mpc.DataUpdatePower, mgcp.DataUpdatePower: - c.dataUpdatePower(entity) + c.maDataUpdatePower(entity) case mpc.DataUpdateEnergyConsumed, mgcp.DataUpdateEnergyConsumed: - c.dataUpdateEnergyConsumed(entity) + c.maDataUpdateEnergyConsumed(entity) case mpc.DataUpdateCurrentsPerPhase, mgcp.DataUpdateCurrentPerPhase: - c.dataUpdateCurrentPerPhase(entity) + c.maDataUpdateCurrentPerPhase(entity) case mpc.DataUpdateVoltagePerPhase, mgcp.DataUpdateVoltagePerPhase: - c.dataUpdateVoltagePerPhase(entity) + c.maDataUpdateVoltagePerPhase(entity) + + // Energy Guard + case lpc.UseCaseSupportUpdate: + c.egLpcUseCaseSupportUpdate(entity) + case lpc.DataUpdateLimit: + c.egLpcDataUpdateLimit(entity) } } -func (c *EEBus) dataUpdatePower(entity spineapi.EntityRemoteInterface) { +func (c *EEBus) maDataUpdatePower(entity spineapi.EntityRemoteInterface) { data, err := c.mm.Power(entity) if err != nil { c.log.ERROR.Println("Power:", err) @@ -129,7 +151,7 @@ func (c *EEBus) dataUpdatePower(entity spineapi.EntityRemoteInterface) { c.power.Set(data) } -func (c *EEBus) dataUpdateEnergyConsumed(entity spineapi.EntityRemoteInterface) { +func (c *EEBus) maDataUpdateEnergyConsumed(entity spineapi.EntityRemoteInterface) { data, err := c.mm.EnergyConsumed(entity) if err != nil { c.log.ERROR.Println("EnergyConsumed:", err) @@ -140,7 +162,7 @@ func (c *EEBus) dataUpdateEnergyConsumed(entity spineapi.EntityRemoteInterface) c.energy.Set(data / 1000) } -func (c *EEBus) dataUpdateCurrentPerPhase(entity spineapi.EntityRemoteInterface) { +func (c *EEBus) maDataUpdateCurrentPerPhase(entity spineapi.EntityRemoteInterface) { data, err := c.mm.CurrentPerPhase(entity) if err != nil { c.log.ERROR.Println("CurrentPerPhase:", err) @@ -149,7 +171,7 @@ func (c *EEBus) dataUpdateCurrentPerPhase(entity spineapi.EntityRemoteInterface) c.currents.Set(data) } -func (c *EEBus) dataUpdateVoltagePerPhase(entity spineapi.EntityRemoteInterface) { +func (c *EEBus) maDataUpdateVoltagePerPhase(entity spineapi.EntityRemoteInterface) { data, err := c.mm.VoltagePerPhase(entity) if err != nil { c.log.ERROR.Println("VoltagePerPhase:", err) @@ -200,3 +222,77 @@ func (c *EEBus) Voltages() (float64, float64, float64, error) { } return res[0], res[1], res[2], nil } + +// +// Energy Guard +// + +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 +} + +var _ api.Dimmer = (*EEBus)(nil) + +// Dimmed implements the api.Dimmer interface +func (c *EEBus) Dimmed() (bool, error) { + c.mu.Lock() + defer c.mu.Unlock() + + // Check if limit is active and has a valid power value + return c.consumptionLimit != nil && c.consumptionLimit.IsActive && c.consumptionLimit.Value > 0, nil +} + +// Dim implements the api.Dimmer interface +func (c *EEBus) Dim(dim bool) error { + // Sets or removes the consumption power limit + + // TODO: change api.Dimmer to make limit configurable + // For now, we use a fixed safe limit of 0W + limit := 0.0 + + var value float64 + if dim { + value = limit + } + + c.mu.Lock() + defer c.mu.Unlock() + + if c.egLpcEntity == nil { + return errors.New("not connected") + } + + if !slices.Contains(c.eg.EgLPCInterface.AvailableScenariosForEntity(c.egLpcEntity), 1) { + return errors.New("scenario 1 not supported") + } + + _, err := c.eg.EgLPCInterface.WriteConsumptionLimit(c.egLpcEntity, ucapi.LoadLimit{ + Value: value, + IsActive: dim, + }, func(result model.ResultDataType) { + if result.ErrorNumber != nil { + c.log.ERROR.Println("ErrorNumber", *result.ErrorNumber) + } + if result.Description != nil { + c.log.ERROR.Println("Description", *result.Description) + } + }) + + return err +} diff --git a/server/eebus/eebus.go b/server/eebus/eebus.go index f3f340fef..55317673c 100644 --- a/server/eebus/eebus.go +++ b/server/eebus/eebus.go @@ -22,6 +22,7 @@ import ( "github.com/enbility/eebus-go/usecases/cem/oscev" "github.com/enbility/eebus-go/usecases/cs/lpc" "github.com/enbility/eebus-go/usecases/cs/lpp" + eglpc "github.com/enbility/eebus-go/usecases/eg/lpc" "github.com/enbility/eebus-go/usecases/ma/mgcp" "github.com/enbility/eebus-go/usecases/ma/mpc" shipapi "github.com/enbility/ship-go/api" @@ -60,12 +61,18 @@ type MonitoringAppliance struct { ucapi.MaMPCInterface } +// Energy Guard +type EnergyGuard struct { + ucapi.EgLPCInterface +} + type EEBus struct { service eebusapi.ServiceInterface cem CustomerEnergyManagement cs ControllableSystem ma MonitoringAppliance + eg EnergyGuard mux sync.Mutex log *util.Logger @@ -170,6 +177,11 @@ func NewServer(other Config) (*EEBus, error) { MaMPCInterface: mpc.NewMPC(localEntity, c.ucCallback), } + // energy guard + c.eg = EnergyGuard{ + EgLPCInterface: eglpc.NewLPC(localEntity, c.ucCallback), + } + // register use cases for _, uc := range []eebusapi.UseCaseInterface{ c.cem.EvseCC, c.cem.EvCC, @@ -177,6 +189,7 @@ func NewServer(other Config) (*EEBus, error) { c.cem.OscEV, c.cem.EvSoc, c.cs.CsLPCInterface, c.cs.CsLPPInterface, c.ma.MaMGCPInterface, c.ma.MaMPCInterface, + c.eg.EgLPCInterface, } { c.service.AddUseCase(uc) } @@ -230,6 +243,10 @@ func (c *EEBus) MonitoringAppliance() *MonitoringAppliance { return &c.ma } +func (c *EEBus) EnergyGuard() *EnergyGuard { + return &c.eg +} + func (c *EEBus) Run() { c.service.Start() }