diff --git a/charger/eebus.go b/charger/eebus.go index e611eca97..8a05e6ea4 100644 --- a/charger/eebus.go +++ b/charger/eebus.go @@ -316,7 +316,7 @@ func (c *EEBus) Enable(enable bool) error { // send current charging power limits to the EV func (c *EEBus) writeCurrentLimitData(evEntity spineapi.EntityRemoteInterface, current float64) error { // check if the EVSE supports overload protection limits - if !c.cem.OpEV.IsScenarioAvailableAtEntity(evEntity, 1) { + if !c.cem.OpEV.IsScenarioAvailableAtEntity(evEntity, eebus.OPEVScenarioObligationLimit) { return api.ErrNotAvailable } @@ -362,7 +362,7 @@ func (c *EEBus) writeCurrentLimitData(evEntity spineapi.EntityRemoteInterface, c // An active recommendation triggers the EV to charge with surplus energy. // An inactive recommendation is equivalent to no recommendation existing. func (c *EEBus) writeOscevLimits(evEntity spineapi.EntityRemoteInterface, current float64) { - if !c.cem.OscEV.IsScenarioAvailableAtEntity(evEntity, 1) { + if !c.cem.OscEV.IsScenarioAvailableAtEntity(evEntity, eebus.OSCEVScenarioRecommendationLimit) { return } @@ -430,7 +430,7 @@ func (c *EEBus) currentPower() (float64, error) { // does the EVSE provide power data? var powers []float64 - if c.cem.EvCem.IsScenarioAvailableAtEntity(evEntity, 2) { + if c.cem.EvCem.IsScenarioAvailableAtEntity(evEntity, eebus.EVCEMScenarioPowerTotal) { // is power data available for real? Elli Gen1 says it supports it, but doesn't provide any data if powerData, err := c.cem.EvCem.PowerPerPhase(evEntity); err == nil { powers = powerData @@ -438,7 +438,7 @@ func (c *EEBus) currentPower() (float64, error) { } // if no power data is available, and currents are reported to be supported, use currents - if len(powers) == 0 && c.cem.EvCem.IsScenarioAvailableAtEntity(evEntity, 1) { + if len(powers) == 0 && c.cem.EvCem.IsScenarioAvailableAtEntity(evEntity, eebus.EVCEMScenarioPowerPerPhase) { // no power provided, calculate from current if currents, err := c.cem.EvCem.CurrentPerPhase(evEntity); err == nil { for _, current := range currents { @@ -462,7 +462,7 @@ func (c *EEBus) chargedEnergy() (float64, error) { return 0, nil } - if !c.cem.EvCem.IsScenarioAvailableAtEntity(evEntity, 3) { + if !c.cem.EvCem.IsScenarioAvailableAtEntity(evEntity, eebus.EVCEMScenarioEnergy) { return 0, api.ErrNotAvailable } @@ -482,7 +482,7 @@ func (c *EEBus) currents() (float64, float64, float64, error) { } // check if the EVSE supports currents - if !c.cem.EvCem.IsScenarioAvailableAtEntity(evEntity, 1) { + if !c.cem.EvCem.IsScenarioAvailableAtEntity(evEntity, eebus.EVCEMScenarioPowerPerPhase) { return 0, 0, 0, api.ErrNotAvailable } @@ -538,7 +538,7 @@ func (c *EEBus) Soc() (float64, error) { return 0, api.ErrNotAvailable } - if !c.cem.EvSoc.IsScenarioAvailableAtEntity(evEntity, 1) { + if !c.cem.EvSoc.IsScenarioAvailableAtEntity(evEntity, eebus.EVSOCScenarioStateOfCharge) { return 0, api.ErrNotAvailable } diff --git a/meter/eebus.go b/meter/eebus.go index 3ad07771b..13dfe2cde 100644 --- a/meter/eebus.go +++ b/meter/eebus.go @@ -29,6 +29,7 @@ type EEBus struct { ma *eebus.MonitoringAppliance eg *eebus.EnergyGuard mm measurements + scenarios maScenarios mu sync.Mutex maEntity spineapi.EntityRemoteInterface @@ -36,6 +37,31 @@ type EEBus struct { egLppEntity spineapi.EntityRemoteInterface } +// maScenarios holds the spec scenario numbers for the active monitoring use case. +// MGCP and MPC use different scenario numbers for the same physical quantity, so +// IsScenarioAvailableAtEntity must be called with the per-UC value. +type maScenarios struct { + power uint + energy uint + currents uint + voltages uint +} + +var ( + mpcScenarios = maScenarios{ + power: eebus.MPCScenarioPower, + energy: eebus.MPCScenarioEnergyConsumed, + currents: eebus.MPCScenarioCurrentPerPhase, + voltages: eebus.MPCScenarioVoltagePerPhase, + } + mgcpScenarios = maScenarios{ + power: eebus.MGCPScenarioPower, + energy: eebus.MGCPScenarioEnergyConsumed, + currents: eebus.MGCPScenarioCurrentPerPhase, + voltages: eebus.MGCPScenarioVoltagePerPhase, + } +) + type measurements interface { eebusapi.UseCaseBaseInterface Power(entity spineapi.EntityRemoteInterface) (float64, error) @@ -76,10 +102,12 @@ func NewEEBus(ctx context.Context, ski, ip string, usage *templates.Usage) (api. // Use MGCP only for explicit grid usage, MPC for everything else (default) useCase := "mpc" mm := measurements(ma.MaMPCInterface) + scenarios := mpcScenarios if usage != nil && *usage == templates.UsageGrid { useCase = "mgcp" mm = ma.MaMGCPInterface + scenarios = mgcpScenarios } c := &EEBus{ @@ -87,6 +115,7 @@ func NewEEBus(ctx context.Context, ski, ip string, usage *templates.Usage) (api. ma: ma, eg: eebus.Instance.EnergyGuard(), mm: mm, + scenarios: scenarios, connector: eebus.NewConnector(), } @@ -144,13 +173,13 @@ func (c *EEBus) readValue(scenario uint, update func(entity spineapi.EntityRemot var _ api.Meter = (*EEBus)(nil) func (c *EEBus) CurrentPower() (float64, error) { - return c.readValue(1, c.mm.Power) + return c.readValue(c.scenarios.power, c.mm.Power) } var _ api.MeterEnergy = (*EEBus)(nil) func (c *EEBus) TotalEnergy() (float64, error) { - return c.readValue(2, c.mm.EnergyConsumed) + return c.readValue(c.scenarios.energy, c.mm.EnergyConsumed) } func (c *EEBus) readPhases(scenario uint, update func(entity spineapi.EntityRemoteInterface) ([]float64, error)) (float64, float64, float64, error) { @@ -184,13 +213,13 @@ func (c *EEBus) readPhases(scenario uint, update func(entity spineapi.EntityRemo var _ api.PhaseCurrents = (*EEBus)(nil) func (c *EEBus) Currents() (float64, float64, float64, error) { - return c.readPhases(3, c.mm.CurrentPerPhase) + return c.readPhases(c.scenarios.currents, c.mm.CurrentPerPhase) } var _ api.PhaseVoltages = (*EEBus)(nil) func (c *EEBus) Voltages() (float64, float64, float64, error) { - return c.readPhases(4, c.mm.VoltagePerPhase) + return c.readPhases(c.scenarios.voltages, c.mm.VoltagePerPhase) } var _ api.Dimmer = (*EEBus)(nil) @@ -200,7 +229,7 @@ func (c *EEBus) Dimmed() (bool, error) { c.mu.Lock() defer c.mu.Unlock() - limit, err := eebusReadValue(c.eg.EgLPCInterface, c.egLpcEntity, 1, c.eg.EgLPCInterface.ConsumptionLimit) + limit, err := eebusReadValue(c.eg.EgLPCInterface, c.egLpcEntity, eebus.LPCScenarioLimit, c.eg.EgLPCInterface.ConsumptionLimit) if err != nil { return false, err } @@ -225,7 +254,7 @@ func (c *EEBus) Dim(dim bool) error { c.mu.Lock() defer c.mu.Unlock() - if c.egLpcEntity == nil || !c.eg.EgLPCInterface.IsScenarioAvailableAtEntity(c.egLpcEntity, 1) { + if c.egLpcEntity == nil || !c.eg.EgLPCInterface.IsScenarioAvailableAtEntity(c.egLpcEntity, eebus.LPCScenarioLimit) { return api.ErrNotAvailable } @@ -244,7 +273,7 @@ func (c *EEBus) Curtailed() (bool, error) { c.mu.Lock() defer c.mu.Unlock() - limit, err := eebusReadValue(c.eg.EgLPPInterface, c.egLppEntity, 1, c.eg.EgLPPInterface.ProductionLimit) + limit, err := eebusReadValue(c.eg.EgLPPInterface, c.egLppEntity, eebus.LPPScenarioLimit, c.eg.EgLPPInterface.ProductionLimit) if err != nil { return false, err } @@ -269,7 +298,7 @@ func (c *EEBus) Curtail(curtail bool) error { c.mu.Lock() defer c.mu.Unlock() - if c.egLppEntity == nil || !c.eg.EgLPPInterface.IsScenarioAvailableAtEntity(c.egLppEntity, 1) { + if c.egLppEntity == nil || !c.eg.EgLPPInterface.IsScenarioAvailableAtEntity(c.egLppEntity, eebus.LPPScenarioLimit) { return api.ErrNotAvailable } diff --git a/server/eebus/scenarios.go b/server/eebus/scenarios.go new file mode 100644 index 000000000..566319eb2 --- /dev/null +++ b/server/eebus/scenarios.go @@ -0,0 +1,72 @@ +package eebus + +// EEBUS use case scenario numbers per the respective Use Case Technical Specifications. +// +// Spec scenario numbers diverge between use cases (e.g. MPC scenario 1 = active power, +// MGCP scenario 1 = power factor; MPC scenario 2 = energy, MGCP scenario 2 = active power). +// Passing the wrong number to IsScenarioAvailableAtEntity gates reads on the wrong feature. +// +// Each block mirrors the scenarios registered in the corresponding eebus-go usecase, which +// in turn matches the EEBus UC TS document. + +// MGCP — Monitoring of Grid Connection Point (UC TS v1.0.0) +const ( + MGCPScenarioPowerFactor uint = 1 // S1 power factor (cos phi) + MGCPScenarioPower uint = 2 // S2 active power per phase + total + MGCPScenarioEnergyFeedIn uint = 3 // S3 total feed-in energy + MGCPScenarioEnergyConsumed uint = 4 // S4 total consumed energy + MGCPScenarioCurrentPerPhase uint = 5 // S5 phase-specific currents + MGCPScenarioVoltagePerPhase uint = 6 // S6 phase-specific voltages + MGCPScenarioFrequency uint = 7 // S7 frequency +) + +// MPC — Monitoring of Power Consumption (UC TS v1.0.0) +const ( + MPCScenarioPower uint = 1 // S1 active power per phase + total + MPCScenarioEnergyConsumed uint = 2 // S2 total consumed energy + MPCScenarioCurrentPerPhase uint = 3 // S3 phase-specific currents + MPCScenarioVoltagePerPhase uint = 4 // S4 phase-specific voltages + MPCScenarioFrequency uint = 5 // S5 frequency +) + +// LPC — Limitation of Power Consumption (UC TS v1.0.0). Same scenario layout for CS and EG roles. +const ( + LPCScenarioLimit uint = 1 // S1 LoadControl: consumption limit + LPCScenarioFailsafe uint = 2 // S2 DeviceConfiguration: failsafe values + LPCScenarioHeartbeat uint = 3 // S3 DeviceDiagnosis: heartbeat + LPCScenarioElectricalConnection uint = 4 // S4 ElectricalConnection (optional) +) + +// LPP — Limitation of Power Production (UC TS v1.0.0). Same scenario layout for CS and EG roles. +const ( + LPPScenarioLimit uint = 1 // S1 LoadControl: production limit + LPPScenarioFailsafe uint = 2 // S2 DeviceConfiguration: failsafe values + LPPScenarioHeartbeat uint = 3 // S3 DeviceDiagnosis: heartbeat + LPPScenarioElectricalConnection uint = 4 // S4 ElectricalConnection (optional) +) + +// OPEV — Overload Protection by EV Charging Current Curtailment (UC TS v1.0.1) +const ( + OPEVScenarioObligationLimit uint = 1 // S1 LoadControl + ElectricalConnection + OPEVScenarioChargingState uint = 2 // S2 charging state + OPEVScenarioChargingPlan uint = 3 // S3 charging plan +) + +// OSCEV — Optimization of Self-Consumption during EV Charging (UC TS v1.0.1) +const ( + OSCEVScenarioRecommendationLimit uint = 1 // S1 LoadControl + ElectricalConnection + OSCEVScenarioChargingState uint = 2 // S2 charging state + OSCEVScenarioChargingPlan uint = 3 // S3 charging plan +) + +// EVCEM — Measurement of Electricity during EV Charging (UC TS v1.0.1) +const ( + EVCEMScenarioPowerPerPhase uint = 1 // S1 phase-specific active power + ElectricalConnection (currents) + EVCEMScenarioPowerTotal uint = 2 // S2 total active power only + EVCEMScenarioEnergy uint = 3 // S3 charging energy summary +) + +// EVSOC — EV State of Charge (UC TS v1.0.0 RC1) +const ( + EVSOCScenarioStateOfCharge uint = 1 // S1 state of charge +)