diff --git a/usecases/README.md b/usecases/README.md index 47a977b1..88166c85 100644 --- a/usecases/README.md +++ b/usecases/README.md @@ -34,3 +34,8 @@ Actors: Use Cases: - `mpc`: Monitoring of Power Consumption - `mgcp`: Monitoring of Grid Connection Point + +- `mu`: Monitored Unit + + Use Cases: + - `mpc`: Monitoring of Power Consumption diff --git a/usecases/api/mu_mpc.go b/usecases/api/mu_mpc.go new file mode 100644 index 00000000..a4ba093f --- /dev/null +++ b/usecases/api/mu_mpc.go @@ -0,0 +1,199 @@ +package api + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/spine-go/model" + "time" +) + +// Actor: Monitoring Unit +// UseCase: Monitoring of Power Consumption +type MuMPCInterface interface { + // ------------------------- Getters ------------------------- // + + // Scenario 1 + + // get the momentary active power consumption or production + // + // possible errors: + // - ErrMissingData if the id is not available + // - and others + Power() (float64, error) + + // get the momentary active power consumption or production per phase + // + // possible errors: + // - ErrMissingData if the id is not available + // - and others + PowerPerPhase() ([]float64, error) + + // Scenario 2 + + // get the total feed in energy + // + // - negative values are used for production + // + // possible errors: + // - ErrMissingData if the id is not available + // - and others + EnergyProduced() (float64, error) + + // get the total feed in energy + // + // - negative values are used for production + // + // possible errors: + // - ErrMissingData if the id is not available + // - and others + EnergyConsumed() (float64, error) + + // Scenario 3 + + // get the momentary phase specific current consumption or production + // + // - positive values are used for consumption + // - negative values are used for production + // + // possible errors: + // - ErrMissingData if the id is not available + // - and others + CurrentPerPhase() ([]float64, error) + + // Scenario 4 + + // get the phase specific voltage details + // + // possible errors: + // - ErrMissingData if the id is not available + // - and others + VoltagePerPhase() ([]float64, error) + + // Scenario 5 + + // get frequency + // + // possible errors: + // - ErrMissingData if the id is not available + // - and others + Frequency() (float64, error) + + // ------------------------- Setters ------------------------- // + + // use Update to update the measurement data + // use it like this: + // + // mpc.Update( + // mpc.UpdateDataPowerTotal(1000, nil, nil), + // mpc.UpdateDataPowerPhaseA(500, nil, nil), + // ... + // ) + // + // possible errors: + // - ErrMissingData if the id is not available + // - and others + Update(data ...api.MeasurementDataForID) error + + // Scenario 1 + + // use UpdateDataPowerTotal in Update to set the momentary active power consumption or production + // The timestamp is optional and can be nil + // The valueState shall be set if it differs from the normal valueState otherwise it can be nil + UpdateDataPowerTotal(value float64, timestamp *time.Time, valueState *model.MeasurementValueStateType) api.MeasurementDataForID + + // use UpdateDataPowerPhaseA in Update to set the momentary active power consumption or production per phase + // The timestamp is optional and can be nil + // The valueState shall be set if it differs from the normal valueState otherwise it can be nil + UpdateDataPowerPhaseA(value float64, timestamp *time.Time, valueState *model.MeasurementValueStateType) api.MeasurementDataForID + + // use UpdateDataPowerPhaseB in Update to set the momentary active power consumption or production per phase + // The timestamp is optional and can be nil + // The valueState shall be set if it differs from the normal valueState otherwise it can be nil + UpdateDataPowerPhaseB(value float64, timestamp *time.Time, valueState *model.MeasurementValueStateType) api.MeasurementDataForID + + // use UpdateDataPowerPhaseC in Update to set the momentary active power consumption or production per phase + // The timestamp is optional and can be nil + // The valueState shall be set if it differs from the normal valueState otherwise it can be nil + UpdateDataPowerPhaseC(value float64, timestamp *time.Time, valueState *model.MeasurementValueStateType) api.MeasurementDataForID + + // Scenario 2 + + // use UpdateDataEnergyConsumed in Update to set the total feed in energy + // The timestamp is optional and can be nil + // The valueState shall be set if it differs from the normal valueState otherwise it can be nil + // The evaluationStart and End are optional and can be nil (both must be set to be used) + UpdateDataEnergyConsumed( + value float64, + timestamp *time.Time, + valueState *model.MeasurementValueStateType, + evaluationStart *time.Time, + evaluationEnd *time.Time, + ) api.MeasurementDataForID + + // use UpdateDataEnergyProduced in Update to set the total feed in energy + // The timestamp is optional and can be nil + // The valueState shall be set if it differs from the normal valueState otherwise it can be nil + // The evaluationStart and End are optional and can be nil (both must be set to be used) + UpdateDataEnergyProduced( + value float64, + timestamp *time.Time, + valueState *model.MeasurementValueStateType, + evaluationStart *time.Time, + evaluationEnd *time.Time, + ) api.MeasurementDataForID + + // Scenario 3 + + // use UpdateDataCurrentPhaseA in Update to set the momentary phase specific current consumption or production + // The timestamp is optional and can be nil + // The valueState shall be set if it differs from the normal valueState otherwise it can be nil + UpdateDataCurrentPhaseA(value float64, timestamp *time.Time, valueState *model.MeasurementValueStateType) api.MeasurementDataForID + + // use UpdateDataCurrentPhaseB in Update to set the momentary phase specific current consumption or production + // The timestamp is optional and can be nil + // The valueState shall be set if it differs from the normal valueState otherwise it can be nil + UpdateDataCurrentPhaseB(value float64, timestamp *time.Time, valueState *model.MeasurementValueStateType) api.MeasurementDataForID + + // use UpdateDataCurrentPhaseC in Update to set the momentary phase specific current consumption or production + // The timestamp is optional and can be nil + // The valueState shall be set if it differs from the normal valueState otherwise it can be nil + UpdateDataCurrentPhaseC(value float64, timestamp *time.Time, valueState *model.MeasurementValueStateType) api.MeasurementDataForID + + // Scenario 4 + + // use UpdateDataVoltagePhaseA in Update to set the phase specific voltage details + // The timestamp is optional and can be nil + // The valueState shall be set if it differs from the normal valueState otherwise it can be nil + UpdateDataVoltagePhaseA(value float64, timestamp *time.Time, valueState *model.MeasurementValueStateType) api.MeasurementDataForID + + // use UpdateDataVoltagePhaseB in Update to set the phase specific voltage details + // The timestamp is optional and can be nil + // The valueState shall be set if it differs from the normal valueState otherwise it can be nil + UpdateDataVoltagePhaseB(value float64, timestamp *time.Time, valueState *model.MeasurementValueStateType) api.MeasurementDataForID + + // use UpdateDataVoltagePhaseC in Update to set the phase specific voltage details + // The timestamp is optional and can be nil + // The valueState shall be set if it differs from the normal valueState otherwise it can be nil + UpdateDataVoltagePhaseC(value float64, timestamp *time.Time, valueState *model.MeasurementValueStateType) api.MeasurementDataForID + + // use UpdateDataVoltagePhaseAToB in Update to set the phase specific voltage details + // The timestamp is optional and can be nil + // The valueState shall be set if it differs from the normal valueState otherwise it can be nil + UpdateDataVoltagePhaseAToB(value float64, timestamp *time.Time, valueState *model.MeasurementValueStateType) api.MeasurementDataForID + + // use UpdateDataVoltagePhaseBToC in Update to set the phase specific voltage details + // The timestamp is optional and can be nil + // The valueState shall be set if it differs from the normal valueState otherwise it can be nil + UpdateDataVoltagePhaseBToC(value float64, timestamp *time.Time, valueState *model.MeasurementValueStateType) api.MeasurementDataForID + + // use UpdateDataVoltagePhaseCToA in Update to set the phase specific voltage details + // The timestamp is optional and can be nil + // The valueState shall be set if it differs from the normal valueState otherwise it can be nil + UpdateDataVoltagePhaseCToA(value float64, timestamp *time.Time, valueState *model.MeasurementValueStateType) api.MeasurementDataForID + + // Scenario 5 + + // use AcFrequency in Update to set the frequency + // The timestamp is optional and can be nil + // The valueState shall be set if it differs from the normal valueState otherwise it can be nil + UpdateDataFrequency(value float64, timestamp *time.Time, valueState *model.MeasurementValueStateType) api.MeasurementDataForID +} diff --git a/usecases/cem/cevc/usecase.go b/usecases/cem/cevc/usecase.go index 4996c0f8..33acc9b0 100644 --- a/usecases/cem/cevc/usecase.go +++ b/usecases/cem/cevc/usecase.go @@ -64,6 +64,7 @@ func NewCEVC(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventC UseCaseSupportUpdate, validActorTypes, validEntityTypes, + false, ) uc := &CEVC{ diff --git a/usecases/cem/evcc/usecase.go b/usecases/cem/evcc/usecase.go index 0fcdbad6..fb72aa7b 100644 --- a/usecases/cem/evcc/usecase.go +++ b/usecases/cem/evcc/usecase.go @@ -76,6 +76,7 @@ func NewEVCC( UseCaseSupportUpdate, validActorTypes, validEntityTypes, + false, ) uc := &EVCC{ diff --git a/usecases/cem/evcem/usecase.go b/usecases/cem/evcem/usecase.go index c87a9438..7fb10121 100644 --- a/usecases/cem/evcem/usecase.go +++ b/usecases/cem/evcem/usecase.go @@ -58,7 +58,9 @@ func NewEVCEM(service api.ServiceInterface, localEntity spineapi.EntityLocalInte eventCB, UseCaseSupportUpdate, validActorTypes, - validEntityTypes) + validEntityTypes, + false, + ) uc := &EVCEM{ UseCaseBase: usecase, diff --git a/usecases/cem/evsecc/usecase.go b/usecases/cem/evsecc/usecase.go index 9a5423a8..88da7d54 100644 --- a/usecases/cem/evsecc/usecase.go +++ b/usecases/cem/evsecc/usecase.go @@ -45,7 +45,9 @@ func NewEVSECC(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEven eventCB, UseCaseSupportUpdate, validActorTypes, - validEntityTypes) + validEntityTypes, + false, + ) uc := &EVSECC{ UseCaseBase: usecase, diff --git a/usecases/cem/evsoc/usecase.go b/usecases/cem/evsoc/usecase.go index 996c4409..d26f6f52 100644 --- a/usecases/cem/evsoc/usecase.go +++ b/usecases/cem/evsoc/usecase.go @@ -41,6 +41,7 @@ func NewEVSOC(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEvent UseCaseSupportUpdate, validActorTypes, validEntityTypes, + false, ) uc := &EVSOC{ diff --git a/usecases/cem/opev/usecase.go b/usecases/cem/opev/usecase.go index 2ad79d95..73f93b73 100644 --- a/usecases/cem/opev/usecase.go +++ b/usecases/cem/opev/usecase.go @@ -52,6 +52,7 @@ func NewOPEV(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventC UseCaseSupportUpdate, validActorTypes, validEntityTypes, + false, ) uc := &OPEV{ diff --git a/usecases/cem/oscev/usecase.go b/usecases/cem/oscev/usecase.go index c74a5887..db4627cf 100644 --- a/usecases/cem/oscev/usecase.go +++ b/usecases/cem/oscev/usecase.go @@ -52,6 +52,7 @@ func NewOSCEV(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEvent UseCaseSupportUpdate, validActorTypes, validEntityTypes, + false, ) uc := &OSCEV{ diff --git a/usecases/cem/vabd/usecase.go b/usecases/cem/vabd/usecase.go index f996becc..4a06b0b7 100644 --- a/usecases/cem/vabd/usecase.go +++ b/usecases/cem/vabd/usecase.go @@ -66,6 +66,7 @@ func NewVABD(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventC UseCaseSupportUpdate, validActorTypes, validEntityTypes, + false, ) uc := &VABD{ diff --git a/usecases/cem/vapd/usecase.go b/usecases/cem/vapd/usecase.go index 5e901a76..bce593a8 100644 --- a/usecases/cem/vapd/usecase.go +++ b/usecases/cem/vapd/usecase.go @@ -57,6 +57,7 @@ func NewVAPD(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventC UseCaseSupportUpdate, validActorTypes, validEntityTypes, + false, ) uc := &VAPD{ diff --git a/usecases/cs/lpc/usecase.go b/usecases/cs/lpc/usecase.go index 0626740f..61376726 100644 --- a/usecases/cs/lpc/usecase.go +++ b/usecases/cs/lpc/usecase.go @@ -65,6 +65,7 @@ func NewLPC(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCa UseCaseSupportUpdate, validActorTypes, validEntityTypes, + false, ) uc := &LPC{ diff --git a/usecases/cs/lpp/usecase.go b/usecases/cs/lpp/usecase.go index 9adb0e82..65cdf0ae 100644 --- a/usecases/cs/lpp/usecase.go +++ b/usecases/cs/lpp/usecase.go @@ -64,6 +64,7 @@ func NewLPP(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCa UseCaseSupportUpdate, validActorTypes, validEntityTypes, + false, ) uc := &LPP{ diff --git a/usecases/eg/lpc/usecase.go b/usecases/eg/lpc/usecase.go index b2727628..ffd36c95 100644 --- a/usecases/eg/lpc/usecase.go +++ b/usecases/eg/lpc/usecase.go @@ -60,6 +60,7 @@ func NewLPC(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCa UseCaseSupportUpdate, validActorTypes, validEntityTypes, + false, ) uc := &LPC{ diff --git a/usecases/eg/lpp/usecase.go b/usecases/eg/lpp/usecase.go index 6d7108f6..381afba5 100644 --- a/usecases/eg/lpp/usecase.go +++ b/usecases/eg/lpp/usecase.go @@ -57,7 +57,9 @@ func NewLPP(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCa eventCB, UseCaseSupportUpdate, validActorTypes, - validEntityTypes) + validEntityTypes, + false, + ) uc := &LPP{ UseCaseBase: usecase, diff --git a/usecases/ma/mgcp/usecase.go b/usecases/ma/mgcp/usecase.go index 8fcfa782..075fed38 100644 --- a/usecases/ma/mgcp/usecase.go +++ b/usecases/ma/mgcp/usecase.go @@ -87,7 +87,9 @@ func NewMGCP(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventC eventCB, UseCaseSupportUpdate, validActorTypes, - validEntityTypes) + validEntityTypes, + false, + ) uc := &MGCP{ UseCaseBase: usecase, diff --git a/usecases/ma/mpc/usecase.go b/usecases/ma/mpc/usecase.go index 69a420cf..c09e3bcd 100644 --- a/usecases/ma/mpc/usecase.go +++ b/usecases/ma/mpc/usecase.go @@ -79,7 +79,9 @@ func NewMPC(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCa eventCB, UseCaseSupportUpdate, validActorTypes, - validEntityTypes) + validEntityTypes, + false, + ) uc := &MPC{ UseCaseBase: usecase, diff --git a/usecases/mu/mpc/config.go b/usecases/mu/mpc/config.go new file mode 100644 index 00000000..52113791 --- /dev/null +++ b/usecases/mu/mpc/config.go @@ -0,0 +1,98 @@ +package mpc + +import ( + "strings" + + "github.com/enbility/spine-go/model" +) + +type ConnectedPhases string + +const ( + ConnectedPhasesA ConnectedPhases = "a" + ConnectedPhasesB ConnectedPhases = "b" + ConnectedPhasesC ConnectedPhases = "c" + ConnectedPhasesAB ConnectedPhases = "ab" + ConnectedPhasesBC ConnectedPhases = "bc" + ConnectedPhasesCA ConnectedPhases = "ac" + ConnectedPhasesABC ConnectedPhases = "abc" +) + +// MonitorPowerConfig is the configuration for the monitor use case +// This config is required by the mpc use case and must be used in mpc.NewMPC +type MonitorPowerConfig struct { + ConnectedPhases ConnectedPhases // The phases that are measured + + ValueSourceTotal *model.MeasurementValueSourceType // The source of the values from the acPowerTotal (required) + ValueSourcePhaseA *model.MeasurementValueSourceType // The source of the values from the acPower for phase A (required if the phase is supported) + ValueSourcePhaseB *model.MeasurementValueSourceType // The source of the values from the acPower for phase B (required if the phase is supported) + ValueSourcePhaseC *model.MeasurementValueSourceType // The source of the values from the acPower for phase C (required if the phase is supported) + + ValueConstraintsTotal *model.MeasurementConstraintsDataType // The constraints for the acPowerTotal (optional can be nil) + ValueConstraintsPhaseA *model.MeasurementConstraintsDataType // The constraints for the acPower for phase A (optional can be nil) + ValueConstraintsPhaseB *model.MeasurementConstraintsDataType // The constraints for the acPower for phase B (optional can be nil) + ValueConstraintsPhaseC *model.MeasurementConstraintsDataType // The constraints for the acPower for phase C (optional can be nil) +} + +// MonitorEnergyConfig is the configuration for the monitor use case +// If this config is passed via NewMPC, the use case will support energy monitoring as specified +type MonitorEnergyConfig struct { + ValueSourceProduction *model.MeasurementValueSourceType // The source of the production values (if this is set, the use case will support production) (optional can be nil) + ValueConstraintsProduction *model.MeasurementConstraintsDataType // The constraints for the production values (optional can be nil) (requires ProductionValueSource to be set) + + ValueSourceConsumption *model.MeasurementValueSourceType // The source of the consumption values (if this is set, the use case will support consumption) (optional can be nil) + ValueConstraintsConsumption *model.MeasurementConstraintsDataType // The constraints for the consumption values (optional can be nil) (requires ConsumptionValueSource to be set) +} + +// MonitorCurrentConfig is the configuration for the monitor use case +// If this config is passed via NewMPC, the use case will support current monitoring +// The current phases will be the same as specified in MonitorPowerConfig +type MonitorCurrentConfig struct { + ValueSourcePhaseA *model.MeasurementValueSourceType // The source of the values for phase A (required if the phase is supported) + ValueSourcePhaseB *model.MeasurementValueSourceType // The source of the values for phase B (required if the phase is supported) + ValueSourcePhaseC *model.MeasurementValueSourceType // The source of the values for phase C (required if the phase is supported) + + ValueConstraintsPhaseA *model.MeasurementConstraintsDataType // The constraints for the current for phase A (optional can be nil) (requires ValueSourcePhaseA to be set) + ValueConstraintsPhaseB *model.MeasurementConstraintsDataType // The constraints for the current for phase B (optional can be nil) (requires ValueSourcePhaseB to be set) + ValueConstraintsPhaseC *model.MeasurementConstraintsDataType // The constraints for the current for phase C (optional can be nil) (requires ValueSourcePhaseC to be set) +} + +// MonitorVoltageConfig is the configuration for the monitor use case +// If this config is passed via NewMPC, the use case will support voltage monitoring +// The voltage phases will be the same as specified in MonitorPowerConfig +type MonitorVoltageConfig struct { + ValueSourcePhaseA *model.MeasurementValueSourceType // The source of the values for phase A (required if the phase is supported) + ValueSourcePhaseB *model.MeasurementValueSourceType // The source of the values for phase B (required if the phase is supported) + ValueSourcePhaseC *model.MeasurementValueSourceType // The source of the values for phase C (required if the phase is supported) + + ValueConstraintsPhaseA *model.MeasurementConstraintsDataType // The constraints for the voltage for phase A (optional can be nil) (requires ValueSourcePhaseA to be set) + ValueConstraintsPhaseB *model.MeasurementConstraintsDataType // The constraints for the voltage for phase B (optional can be nil) (requires ValueSourcePhaseB to be set) + ValueConstraintsPhaseC *model.MeasurementConstraintsDataType // The constraints for the voltage for phase C (optional can be nil) (requires ValueSourcePhaseC to be set) + + SupportPhaseToPhase bool // Set to true if the use case supports phase to phase voltage monitoring + ValueSourcePhaseAToB *model.MeasurementValueSourceType // The source of the values for phase A to B (required if the phases are supported and SupportPhaseToPhase is true) + ValueSourcePhaseBToC *model.MeasurementValueSourceType // The source of the values for phase B to C (required if the phases are supported and SupportPhaseToPhase is true) + ValueSourcePhaseCToA *model.MeasurementValueSourceType // The source of the values for phase C to A (required if the phases are supported and SupportPhaseToPhase is true) + + ValueConstraintsPhaseAToB *model.MeasurementConstraintsDataType // The constraints for the voltage for phase A to B (optional can be nil) (requires ValueSourcePhaseAToB to be set) + ValueConstraintsPhaseBToC *model.MeasurementConstraintsDataType // The constraints for the voltage for phase B to C (optional can be nil) (requires ValueSourcePhaseBToC to be set) + ValueConstraintsPhaseCToA *model.MeasurementConstraintsDataType // The constraints for the voltage for phase C to A (optional can be nil) (requires ValueSourcePhaseCToA to be set) +} + +// MonitorFrequencyConfig is the configuration for the monitor use case +type MonitorFrequencyConfig struct { + ValueSource *model.MeasurementValueSourceType // The source of the values (required) + ValueConstraints *model.MeasurementConstraintsDataType // The constraints for the frequency values (optional can be nil) +} + +func (c *MonitorPowerConfig) SupportsPhases(phase []string) bool { + phasesString := string(c.ConnectedPhases) + supports := true + for _, p := range phase { + if !strings.Contains(phasesString, p) { + supports = false + break + } + } + return supports +} diff --git a/usecases/mu/mpc/events.go b/usecases/mu/mpc/events.go new file mode 100644 index 00000000..c05e05aa --- /dev/null +++ b/usecases/mu/mpc/events.go @@ -0,0 +1,10 @@ +package mpc + +import ( + spineapi "github.com/enbility/spine-go/api" +) + +// handle SPINE events +func (e *MPC) HandleEvent(payload spineapi.EventPayload) { + // No events supported +} diff --git a/usecases/mu/mpc/public.go b/usecases/mu/mpc/public.go new file mode 100644 index 00000000..b3302a7d --- /dev/null +++ b/usecases/mu/mpc/public.go @@ -0,0 +1,562 @@ +package mpc + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/server" + "github.com/enbility/spine-go/model" + "time" +) + +// ------------------------- Getters ------------------------- // + +// Scenario 1 + +// get the momentary active power consumption or production +// +// possible errors: +// - ErrMissingData if the id is not available +// - and others +func (e *MPC) Power() (float64, error) { + if e.acPowerTotal == nil { + return 0, api.ErrMissingData + } + + return e.getMeasurementDataForId(e.acPowerTotal) +} + +// get the momentary active power consumption or production per phase +// +// possible errors: +// - ErrMissingData if the id is not available +// - and others +func (e *MPC) PowerPerPhase() ([]float64, error) { + powerPerPhase := make([]float64, 0) + + for _, id := range e.acPower { + if id != nil { + power, err := e.getMeasurementDataForId(id) + if err != nil { + return nil, err + } + powerPerPhase = append(powerPerPhase, power) + } + } + + return powerPerPhase, nil +} + +// Scenario 2 + +// get the total feed in energy +// +// - negative values are used for production +// +// possible errors: +// - ErrMissingData if the id is not available +// - and others +func (e *MPC) EnergyConsumed() (float64, error) { + if e.acEnergyConsumed == nil { + return 0, api.ErrMissingData + } + + return e.getMeasurementDataForId(e.acEnergyConsumed) +} + +// get the total feed in energy +// +// - negative values are used for production +// +// possible errors: +// - ErrMissingData if the id is not available +// - and others +func (e *MPC) EnergyProduced() (float64, error) { + if e.acEnergyProduced == nil { + return 0, api.ErrMissingData + } + + return e.getMeasurementDataForId(e.acEnergyProduced) +} + +// Scenario 3 + +// get the momentary phase specific current consumption or production +// +// - positive values are used for consumption +// - negative values are used for production +// +// possible errors: +// - ErrMissingData if the id is not available +// - and others +func (e *MPC) CurrentPerPhase() ([]float64, error) { + currentPerPhase := make([]float64, 0) + + for _, id := range e.acCurrent { + if id != nil { + current, err := e.getMeasurementDataForId(id) + if err != nil { + return nil, err + } + currentPerPhase = append(currentPerPhase, current) + } + } + + return currentPerPhase, nil +} + +// Scenario 4 + +// get the phase specific voltage details +// +// possible errors: +// - ErrMissingData if the id is not available +// - and others +func (e *MPC) VoltagePerPhase() ([]float64, error) { + voltagePerPhase := make([]float64, 0) + + for _, id := range e.acVoltage { + if id != nil { + voltage, err := e.getMeasurementDataForId(id) + if err != nil { + return nil, err + } + voltagePerPhase = append(voltagePerPhase, voltage) + } + } + + return voltagePerPhase, nil +} + +// Scenario 5 + +// get frequency +// +// possible errors: +// - ErrMissingData if the id is not available +// - and others +func (e *MPC) Frequency() (float64, error) { + if e.acFrequency == nil { + return 0, api.ErrMissingData + } + + return e.getMeasurementDataForId(e.acFrequency) +} + +// ------------------------- Setters ------------------------- // + +// use MPC.Update to update the measurement data +// use it like this: +// +// mpc.Update( +// mpc.UpdateDataPowerTotal(1000, nil, nil), +// mpc.UpdateDataPowerPhaseA(500, nil, nil), +// ... +// ) +// +// possible errors: +// - ErrMissingData if the id is not available +// - and others +func (e *MPC) Update(measurementDataForIds ...api.MeasurementDataForID) error { + measurements, err := server.NewMeasurement(e.LocalEntity) + if err != nil { + return err + } + + return measurements.UpdateDataForIds(measurementDataForIds) +} + +// Scenario 1 + +// use MPC.UpdateDataPowerTotal in MPC.Update to set the momentary active power consumption or production +// The timestamp is optional and can be nil +// The valueState shall be set if it differs from the normal valueState otherwise it can be nil +func (e *MPC) UpdateDataPowerTotal( + acPowerTotal float64, + timestamp *time.Time, + valueState *model.MeasurementValueStateType, +) api.MeasurementDataForID { + return api.MeasurementDataForID{ + Data: measurementData( + acPowerTotal, + timestamp, + e.powerConfig.ValueSourceTotal, + valueState, + nil, + nil, + ), + Id: *e.acPowerTotal, + } +} + +// use MPC.UpdateDataPowerPhaseA in MPC.Update to set the momentary active power consumption or production per phase +// The timestamp is optional and can be nil +// The valueState shall be set if it differs from the normal valueState otherwise it can be nil +func (e *MPC) UpdateDataPowerPhaseA( + acPowerPhaseA float64, + timestamp *time.Time, + valueState *model.MeasurementValueStateType, +) api.MeasurementDataForID { + if e.acPower[0] == nil { + panic("acPowerPhaseA is not supported, please check the configuration") + } + return api.MeasurementDataForID{ + Data: measurementData( + acPowerPhaseA, + timestamp, + e.powerConfig.ValueSourcePhaseA, + valueState, + nil, + nil, + ), + Id: *e.acPower[0], + } +} + +// use MPC.UpdateDataPowerPhaseB in MPC.Update to set the momentary active power consumption or production per phase +// The timestamp is optional and can be nil +// The valueState shall be set if it differs from the normal valueState otherwise it can be nil +func (e *MPC) UpdateDataPowerPhaseB( + acPowerPhaseB float64, + timestamp *time.Time, + valueState *model.MeasurementValueStateType, +) api.MeasurementDataForID { + if e.acPower[1] == nil { + panic("acPowerPhaseB is not supported, please check the configuration") + } + return api.MeasurementDataForID{ + Data: measurementData( + acPowerPhaseB, + timestamp, + e.powerConfig.ValueSourcePhaseB, + valueState, + nil, + nil, + ), + Id: *e.acPower[1], + } +} + +// use MPC.UpdateDataPowerPhaseC in MPC.Update to set the momentary active power consumption or production per phase +// The timestamp is optional and can be nil +// The valueState shall be set if it differs from the normal valueState otherwise it can be nil +func (e *MPC) UpdateDataPowerPhaseC( + acPowerPhaseC float64, + timestamp *time.Time, + valueState *model.MeasurementValueStateType, +) api.MeasurementDataForID { + if e.acPower[2] == nil { + panic("acPowerPhaseC is not supported, please check the configuration") + } + return api.MeasurementDataForID{ + Data: measurementData( + acPowerPhaseC, + timestamp, + e.powerConfig.ValueSourcePhaseC, + valueState, + nil, + nil, + ), + Id: *e.acPower[2], + } +} + +// Scenario 2 + +// use MPC.UpdateDataEnergyConsumed in MPC.Update to set the total feed in energy +// The timestamp is optional and can be nil +// The valueState shall be set if it differs from the normal valueState otherwise it can be nil +// The evaluationStart and End are optional and can be nil (both must be set to be used) +func (e *MPC) UpdateDataEnergyConsumed( + energyConsumed float64, + timestamp *time.Time, + valueState *model.MeasurementValueStateType, + evaluationStart *time.Time, + evaluationEnd *time.Time, +) api.MeasurementDataForID { + if e.acEnergyConsumed == nil { + panic("acEnergyConsumed is not supported, please check the configuration") + } + return api.MeasurementDataForID{ + Data: measurementData( + energyConsumed, + timestamp, + e.energyConfig.ValueSourceConsumption, + valueState, + evaluationStart, + evaluationEnd, + ), + Id: *e.acEnergyConsumed, + } +} + +// use MPC.MeasuredUpdateDataEnergyProduced in MPC.Update to set the total feed in energy +// The timestamp is optional and can be nil +// The valueState shall be set if it differs from the normal valueState otherwise it can be nil +// The evaluationStart and End are optional and can be nil (both must be set to be used) +func (e *MPC) UpdateDataEnergyProduced( + energyProduced float64, + timestamp *time.Time, + valueState *model.MeasurementValueStateType, + evaluationStart *time.Time, + evaluationEnd *time.Time, +) api.MeasurementDataForID { + if e.acEnergyProduced == nil { + panic("acEnergyProduced is not supported, please check the configuration") + } + return api.MeasurementDataForID{ + Data: measurementData( + energyProduced, + timestamp, + e.energyConfig.ValueSourceProduction, + valueState, + evaluationStart, + evaluationEnd, + ), + Id: *e.acEnergyProduced, + } +} + +// Scenario 3 + +// use MPC.UpdateDataCurrentPhaseA in MPC.Update to set the momentary phase specific current consumption or production +// The timestamp is optional and can be nil +// The valueState shall be set if it differs from the normal valueState otherwise it can be nil +func (e *MPC) UpdateDataCurrentPhaseA( + acCurrentPhaseA float64, + timestamp *time.Time, + valueState *model.MeasurementValueStateType, +) api.MeasurementDataForID { + if e.acCurrent[0] == nil { + panic("acCurrentPhaseA is not supported, please check the configuration") + } + return api.MeasurementDataForID{ + Data: measurementData( + acCurrentPhaseA, + timestamp, + e.currentConfig.ValueSourcePhaseA, + valueState, + nil, + nil, + ), + Id: *e.acCurrent[0], + } +} + +// use MPC.UpdateDataCurrentPhaseB in MPC.Update to set the momentary phase specific current consumption or production +// The timestamp is optional and can be nil +// The valueState shall be set if it differs from the normal valueState otherwise it can be nil +func (e *MPC) UpdateDataCurrentPhaseB( + acCurrentPhaseB float64, + timestamp *time.Time, + valueState *model.MeasurementValueStateType, +) api.MeasurementDataForID { + if e.acCurrent[1] == nil { + panic("acCurrentPhaseB is not supported, please check the configuration") + } + return api.MeasurementDataForID{ + Data: measurementData( + acCurrentPhaseB, + timestamp, + e.currentConfig.ValueSourcePhaseB, + valueState, + nil, + nil, + ), + Id: *e.acCurrent[1], + } +} + +// use MPC.UpdateDataCurrentPhaseC in MPC.Update to set the momentary phase specific current consumption or production +// The timestamp is optional and can be nil +// The valueState shall be set if it differs from the normal valueState otherwise it can be nil +func (e *MPC) UpdateDataCurrentPhaseC( + acCurrentPhaseC float64, + timestamp *time.Time, + valueState *model.MeasurementValueStateType, +) api.MeasurementDataForID { + if e.acCurrent[2] == nil { + panic("acCurrentPhaseC is not supported, please check the configuration") + } + return api.MeasurementDataForID{ + Data: measurementData( + acCurrentPhaseC, + timestamp, + e.currentConfig.ValueSourcePhaseC, + valueState, + nil, + nil, + ), + Id: *e.acCurrent[2], + } +} + +// Scenario 4 + +// use MPC.UpdateDataVoltagePhaseA in MPC.Update to set the phase specific voltage details +// The timestamp is optional and can be nil +// The valueState shall be set if it differs from the normal valueState otherwise it can be nil +func (e *MPC) UpdateDataVoltagePhaseA( + voltagePhaseA float64, + timestamp *time.Time, + valueState *model.MeasurementValueStateType, +) api.MeasurementDataForID { + if e.acVoltage[0] == nil { + panic("acVoltagePhaseA is not supported, please check the configuration") + } + return api.MeasurementDataForID{ + Data: measurementData( + voltagePhaseA, + timestamp, + e.voltageConfig.ValueSourcePhaseA, + valueState, + nil, + nil, + ), + Id: *e.acVoltage[0], + } +} + +// use MPC.UpdateDataVoltagePhaseB in MPC.Update to set the phase specific voltage details +// The timestamp is optional and can be nil +// The valueState shall be set if it differs from the normal valueState otherwise it can be nil +func (e *MPC) UpdateDataVoltagePhaseB( + voltagePhaseB float64, + timestamp *time.Time, + valueState *model.MeasurementValueStateType, +) api.MeasurementDataForID { + if e.acVoltage[1] == nil { + panic("acVoltagePhaseB is not supported, please check the configuration") + } + return api.MeasurementDataForID{ + Data: measurementData( + voltagePhaseB, + timestamp, + e.voltageConfig.ValueSourcePhaseB, + valueState, + nil, + nil, + ), + Id: *e.acVoltage[1], + } +} + +// use MPC.UpdateDataVoltagePhaseC in MPC.Update to set the phase specific voltage details +// The timestamp is optional and can be nil +// The valueState shall be set if it differs from the normal valueState otherwise it can be nil +func (e *MPC) UpdateDataVoltagePhaseC( + voltagePhaseC float64, + timestamp *time.Time, + valueState *model.MeasurementValueStateType, +) api.MeasurementDataForID { + if e.acVoltage[2] == nil { + panic("acVoltagePhaseC is not supported, please check the configuration") + } + return api.MeasurementDataForID{ + Data: measurementData( + voltagePhaseC, + timestamp, + e.voltageConfig.ValueSourcePhaseC, + valueState, + nil, + nil, + ), + Id: *e.acVoltage[2], + } +} + +// use MPC.UpdateDataVoltagePhaseAToB in MPC.Update to set the phase specific voltage details +// The timestamp is optional and can be nil +// The valueState shall be set if it differs from the normal valueState otherwise it can be nil +func (e *MPC) UpdateDataVoltagePhaseAToB( + voltagePhaseAToB float64, + timestamp *time.Time, + valueState *model.MeasurementValueStateType, +) api.MeasurementDataForID { + if e.acVoltage[3] == nil { + panic("acVoltagePhaseAToB is not supported, please check the configuration") + } + return api.MeasurementDataForID{ + Data: measurementData( + voltagePhaseAToB, + timestamp, + e.voltageConfig.ValueSourcePhaseAToB, + valueState, + nil, + nil, + ), + Id: *e.acVoltage[3], + } +} + +// use MPC.UpdateDataVoltagePhaseBToC in MPC.Update to set the phase specific voltage details +// The timestamp is optional and can be nil +// The valueState shall be set if it differs from the normal valueState otherwise it can be nil +func (e *MPC) UpdateDataVoltagePhaseBToC( + voltagePhaseBToC float64, + timestamp *time.Time, + valueState *model.MeasurementValueStateType, +) api.MeasurementDataForID { + if e.acVoltage[4] == nil { + panic("acVoltagePhaseBToC is not supported, please check the configuration") + } + return api.MeasurementDataForID{ + Data: measurementData( + voltagePhaseBToC, + timestamp, + e.voltageConfig.ValueSourcePhaseBToC, + valueState, + nil, + nil, + ), + Id: *e.acVoltage[4], + } +} + +// use MPC.UpdateDataVoltagePhaseCToA in MPC.Update to set the phase specific voltage details +// The timestamp is optional and can be nil +// The valueState shall be set if it differs from the normal valueState otherwise it can be nil +func (e *MPC) UpdateDataVoltagePhaseCToA( + voltagePhaseCToA float64, + timestamp *time.Time, + valueState *model.MeasurementValueStateType, +) api.MeasurementDataForID { + if e.acVoltage[5] == nil { + panic("acVoltagePhaseCToA is not supported, please check the configuration") + } + return api.MeasurementDataForID{ + Data: measurementData( + voltagePhaseCToA, + timestamp, + e.voltageConfig.ValueSourcePhaseCToA, + valueState, + nil, + nil, + ), + Id: *e.acVoltage[5], + } +} + +// Scenario 5 + +// use MPC.UpdateDataFrequency in MPC.Update to set the frequency +// The timestamp is optional and can be nil +// The valueState shall be set if it differs from the normal valueState otherwise it can be nil +func (e *MPC) UpdateDataFrequency( + frequency float64, + timestamp *time.Time, + valueState *model.MeasurementValueStateType, +) api.MeasurementDataForID { + if e.acFrequency == nil { + panic("acFrequency is not supported, please check the configuration") + } + return api.MeasurementDataForID{ + Data: measurementData( + frequency, + timestamp, + e.frequencyConfig.ValueSource, + valueState, + nil, + nil, + ), + Id: *e.acFrequency, + } +} diff --git a/usecases/mu/mpc/public_test.go b/usecases/mu/mpc/public_test.go new file mode 100644 index 00000000..bea94e79 --- /dev/null +++ b/usecases/mu/mpc/public_test.go @@ -0,0 +1,94 @@ +package mpc + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" + "time" +) + +func (s *MuMPCSuite) Test_Power() { + err := s.sut.Update( + s.sut.UpdateDataPowerTotal(5.0, util.Ptr(time.Now()), nil), + ) + assert.Nil(s.T(), err) + + power, err := s.sut.Power() + assert.Nil(s.T(), err) + assert.Equal(s.T(), 5.0, power) +} + +func (s *MuMPCSuite) Test_PowerPerPhase() { + err := s.sut.Update( + s.sut.UpdateDataPowerPhaseA(5.0, util.Ptr(time.Now()), nil), + s.sut.UpdateDataPowerPhaseB(6.0, util.Ptr(time.Now()), nil), + s.sut.UpdateDataPowerPhaseC(7.0, util.Ptr(time.Now()), util.Ptr(model.MeasurementValueStateTypeError)), + ) + assert.Nil(s.T(), err) + + powerPerPhases, err := s.sut.PowerPerPhase() + assert.Nil(s.T(), err) + assert.Equal(s.T(), []float64{5.0, 6.0, 7.0}, powerPerPhases) +} + +func (s *MuMPCSuite) Test_EnergyConsumed() { + err := s.sut.Update( + s.sut.UpdateDataEnergyConsumed(5.0, util.Ptr(time.Now()), nil, util.Ptr(time.Now()), util.Ptr(time.Now())), + ) + assert.Nil(s.T(), err) + + energyConsumed, err := s.sut.EnergyConsumed() + assert.Nil(s.T(), err) + assert.Equal(s.T(), 5.0, energyConsumed) +} + +func (s *MuMPCSuite) Test_EnergyProduced() { + err := s.sut.Update( + s.sut.UpdateDataEnergyProduced(5.0, nil, nil, nil, nil), + ) + assert.Nil(s.T(), err) + + energyProduced, err := s.sut.EnergyProduced() + assert.Nil(s.T(), err) + assert.Equal(s.T(), 5.0, energyProduced) +} + +func (s *MuMPCSuite) Test_CurrentPerPhase() { + err := s.sut.Update( + s.sut.UpdateDataCurrentPhaseA(5.0, nil, nil), + s.sut.UpdateDataCurrentPhaseB(3.0, nil, nil), + s.sut.UpdateDataCurrentPhaseC(1.0, nil, nil), + ) + assert.Nil(s.T(), err) + + currentPerPhases, err := s.sut.CurrentPerPhase() + assert.Nil(s.T(), err) + assert.Equal(s.T(), []float64{5.0, 3.0, 1.0}, currentPerPhases) +} + +func (s *MuMPCSuite) Test_VoltagePerPhase() { + err := s.sut.Update( + s.sut.UpdateDataVoltagePhaseA(5.0, nil, nil), + s.sut.UpdateDataVoltagePhaseB(6.0, nil, nil), + s.sut.UpdateDataVoltagePhaseC(7.0, nil, nil), + s.sut.UpdateDataVoltagePhaseAToB(8.0, nil, nil), + s.sut.UpdateDataVoltagePhaseBToC(9.0, nil, nil), + s.sut.UpdateDataVoltagePhaseCToA(10.0, nil, nil), + ) + assert.Nil(s.T(), err) + + voltagePerPhases, err := s.sut.VoltagePerPhase() + assert.Nil(s.T(), err) + assert.Equal(s.T(), []float64{5.0, 6.0, 7.0, 8.0, 9.0, 10.0}, voltagePerPhases) +} + +func (s *MuMPCSuite) Test_Frequency() { + err := s.sut.Update( + s.sut.UpdateDataFrequency(5.0, nil, nil), + ) + assert.Nil(s.T(), err) + + frequency, err := s.sut.Frequency() + assert.Nil(s.T(), err) + assert.Equal(s.T(), 5.0, frequency) +} diff --git a/usecases/mu/mpc/testhelper_test.go b/usecases/mu/mpc/testhelper_test.go new file mode 100644 index 00000000..c4b4dde4 --- /dev/null +++ b/usecases/mu/mpc/testhelper_test.go @@ -0,0 +1,118 @@ +package mpc + +import ( + "testing" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + shipapi "github.com/enbility/ship-go/api" + "github.com/enbility/ship-go/cert" + spineapi "github.com/enbility/spine-go/api" + spinemocks "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +const remoteSki string = "testremoteski" + +func TestMuMPCSuite(t *testing.T) { + suite.Run(t, new(MuMPCSuite)) +} + +type MuMPCSuite struct { + suite.Suite + + sut *MPC + + service api.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *spinemocks.EntityRemoteInterface + monitoredEntity spineapi.EntityRemoteInterface + loadControlFeature, + deviceDiagnosisFeature, + deviceConfigurationFeature spineapi.FeatureLocalInterface + + eventCalled bool +} + +func (s *MuMPCSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + s.eventCalled = true +} + +func (s *MuMPCSuite) BeforeTest(suiteName, testName string) { + s.eventCalled = false + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := api.NewConfiguration( + "test", "test", "test", "test", + []shipapi.DeviceCategoryType{shipapi.DeviceCategoryTypeEnergyManagementSystem}, + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeInverter}, + 9999, cert, time.Second*4) + + serviceHandler := mocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := spinemocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = spinemocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := spinemocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + localEntity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeInverter) + s.sut, _ = NewMPC( + localEntity, + s.Event, + &MonitorPowerConfig{ + ConnectedPhases: ConnectedPhasesABC, + ValueSourceTotal: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseA: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseB: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseC: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + }, + &MonitorEnergyConfig{ + ValueSourceProduction: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourceConsumption: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + }, + &MonitorCurrentConfig{ + ValueSourcePhaseA: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseB: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseC: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + }, + &MonitorVoltageConfig{ + SupportPhaseToPhase: true, + ValueSourcePhaseA: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseB: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseC: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseAToB: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseBToC: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseCToA: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + }, + &MonitorFrequencyConfig{ + ValueSource: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueConstraints: util.Ptr(model.MeasurementConstraintsDataType{ + ValueRangeMin: model.NewScaledNumberType(0), + ValueRangeMax: model.NewScaledNumberType(100), + ValueStepSize: model.NewScaledNumberType(1), + }), + }, + ) + s.sut.AddFeatures() + s.sut.AddUseCase() + + //s.remoteDevice, s.monitoredEntity = setupDevices(s.service, s.T()) +} diff --git a/usecases/mu/mpc/types.go b/usecases/mu/mpc/types.go new file mode 100644 index 00000000..67f2d3f0 --- /dev/null +++ b/usecases/mu/mpc/types.go @@ -0,0 +1,10 @@ +package mpc + +import "github.com/enbility/eebus-go/api" + +const ( + // Update of the list of remote entities supporting the Use Case + // + // Use `RemoteEntities` to get the current data + UseCaseSupportUpdate api.EventType = "mu-mpc-UseCaseSupportUpdate" +) diff --git a/usecases/mu/mpc/usecase.go b/usecases/mu/mpc/usecase.go new file mode 100644 index 00000000..795c55ae --- /dev/null +++ b/usecases/mu/mpc/usecase.go @@ -0,0 +1,605 @@ +package mpc + +import ( + "errors" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/server" + "github.com/enbility/eebus-go/usecases/usecase" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" +) + +type MPC struct { + *usecase.UseCaseBase + + powerConfig *MonitorPowerConfig + energyConfig *MonitorEnergyConfig + currentConfig *MonitorCurrentConfig + voltageConfig *MonitorVoltageConfig + frequencyConfig *MonitorFrequencyConfig + + acPowerTotal *model.MeasurementIdType + acPower [3]*model.MeasurementIdType + acEnergyConsumed *model.MeasurementIdType + acEnergyProduced *model.MeasurementIdType + acCurrent [3]*model.MeasurementIdType + acVoltage [6]*model.MeasurementIdType // Phase to phase voltages are not supported (yet) + acFrequency *model.MeasurementIdType +} + +// creates a new MPC usecase instance for a MonitoredUnit entity +// +// parameters: +// - localEntity: the local entity for which to construct an MPC instance +// - eventCB: the callback to notify about events for this usecase +// - monitorPowerConfig: (required) configuration parameters for MPC scenario 1 +// - monitorEnergyConfig: (optional) configuration parameters for MPC scenario 2, nil if not supported +// - monitorCurrentConfig: (optional) configuration parameters for MPC scenario 3, nil if not supported +// - monitorVoltageConfig: (optional) configuration parameters for MPC scenario 4, nil if not supported +// - monitorFrequencyConfig: (optional) configuration parameters for MPC scenario, nil if not supported +// +// possible errors: +// - if required fields in parameters are unset +func NewMPC( + localEntity spineapi.EntityLocalInterface, + eventCB api.EntityEventCallback, + monitorPowerConfig *MonitorPowerConfig, + monitorEnergyConfig *MonitorEnergyConfig, + monitorCurrentConfig *MonitorCurrentConfig, + monitorVoltageConfig *MonitorVoltageConfig, + monitorFrequencyConfig *MonitorFrequencyConfig, +) (*MPC, error) { + if monitorPowerConfig == nil { + return nil, errors.New("the monitor power config for the MPC-Use-Case must not be nil") + } + + validActorTypes := []model.UseCaseActorType{model.UseCaseActorTypeMonitoringAppliance} + useCaseScenarios := []api.UseCaseScenario{ + { + Scenario: model.UseCaseScenarioSupportType(1), + Mandatory: true, + ServerFeatures: []model.FeatureTypeType{ + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + }, + }, + } + + if monitorEnergyConfig != nil { + useCaseScenarios = append(useCaseScenarios, api.UseCaseScenario{ + Scenario: model.UseCaseScenarioSupportType(2), + Mandatory: false, + ServerFeatures: []model.FeatureTypeType{ + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + }, + }) + } + + if monitorCurrentConfig != nil { + useCaseScenarios = append(useCaseScenarios, api.UseCaseScenario{ + Scenario: model.UseCaseScenarioSupportType(3), + Mandatory: false, + ServerFeatures: []model.FeatureTypeType{ + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + }, + }) + } + + if monitorVoltageConfig != nil { + useCaseScenarios = append(useCaseScenarios, api.UseCaseScenario{ + Scenario: model.UseCaseScenarioSupportType(4), + Mandatory: false, + ServerFeatures: []model.FeatureTypeType{ + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + }, + }) + } + + if monitorFrequencyConfig != nil { + useCaseScenarios = append(useCaseScenarios, api.UseCaseScenario{ + Scenario: model.UseCaseScenarioSupportType(5), + Mandatory: false, + ServerFeatures: []model.FeatureTypeType{ + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + }, + }) + } + + u := usecase.NewUseCaseBase( + localEntity, + model.UseCaseActorTypeMonitoredUnit, + model.UseCaseNameTypeMonitoringOfPowerConsumption, + "1.0.0", + "release", + useCaseScenarios, + eventCB, + UseCaseSupportUpdate, + validActorTypes, + nil, + true, + ) + + uc := &MPC{ + UseCaseBase: u, + powerConfig: monitorPowerConfig, + energyConfig: monitorEnergyConfig, + currentConfig: monitorCurrentConfig, + voltageConfig: monitorVoltageConfig, + frequencyConfig: monitorFrequencyConfig, + } + + _ = spine.Events.Subscribe(uc) + + return uc, nil +} + +func (e *MPC) AddFeatures() { + // server features + electricalConnectionFeatrue := e.LocalEntity.GetOrAddFeature(model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + electricalConnectionFeatrue.AddFunctionType(model.FunctionTypeElectricalConnectionDescriptionListData, true, false) + electricalConnectionFeatrue.AddFunctionType(model.FunctionTypeElectricalConnectionParameterDescriptionListData, true, false) + + measurementFeature := e.LocalEntity.GetOrAddFeature(model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + measurementFeature.AddFunctionType(model.FunctionTypeMeasurementDescriptionListData, true, false) + measurementFeature.AddFunctionType(model.FunctionTypeMeasurementConstraintsListData, true, false) + measurementFeature.AddFunctionType(model.FunctionTypeMeasurementListData, true, false) + + measurements, err := server.NewMeasurement(e.LocalEntity) + if err != nil { + panic(err) + } + + var phases = [][]string{ + {"a"}, + {"b"}, + {"c"}, + {"a", "b"}, + {"b", "c"}, + {"c", "a"}, + } + + var constraints = make([]model.MeasurementConstraintsDataType, 0) + + e.acPowerTotal = measurements.AddDescription(model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypePower), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + Unit: util.Ptr(model.UnitOfMeasurementTypeW), + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + }) + + if e.powerConfig.ValueConstraintsTotal != nil { + e.powerConfig.ValueConstraintsTotal.MeasurementId = e.acPowerTotal + constraints = append(constraints, *e.powerConfig.ValueConstraintsTotal) + } + + acPowerConstraints := []*model.MeasurementConstraintsDataType{ + e.powerConfig.ValueConstraintsPhaseA, + e.powerConfig.ValueConstraintsPhaseB, + e.powerConfig.ValueConstraintsPhaseC, + } + for id := 0; id < len(e.acPower); id++ { + if e.powerConfig.SupportsPhases(phases[id]) { + e.acPower[id] = measurements.AddDescription(model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypePower), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + Unit: util.Ptr(model.UnitOfMeasurementTypeW), + ScopeType: util.Ptr(model.ScopeTypeTypeACPower), + }) + if acPowerConstraints[id] != nil { + acPowerConstraints[id].MeasurementId = e.acPower[id] + constraints = append(constraints, *acPowerConstraints[id]) + } + } + } + + if e.energyConfig != nil { + if e.energyConfig.ValueSourceConsumption != nil { + e.acEnergyConsumed = measurements.AddDescription(model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + Unit: util.Ptr(model.UnitOfMeasurementTypeWh), + ScopeType: util.Ptr(model.ScopeTypeTypeACEnergyConsumed), + }) + if e.energyConfig.ValueConstraintsConsumption != nil { + e.energyConfig.ValueConstraintsConsumption.MeasurementId = e.acEnergyConsumed + constraints = append(constraints, *e.energyConfig.ValueConstraintsConsumption) + } + } + + if e.energyConfig.ValueSourceProduction != nil { + e.acEnergyProduced = measurements.AddDescription(model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + Unit: util.Ptr(model.UnitOfMeasurementTypeWh), + ScopeType: util.Ptr(model.ScopeTypeTypeACEnergyProduced), + }) + if e.energyConfig.ValueConstraintsProduction != nil { + e.energyConfig.ValueConstraintsProduction.MeasurementId = e.acEnergyProduced + constraints = append(constraints, *e.energyConfig.ValueConstraintsProduction) + } + } + } + + if e.currentConfig != nil { + acCurrentConstraints := []*model.MeasurementConstraintsDataType{ + e.currentConfig.ValueConstraintsPhaseA, + e.currentConfig.ValueConstraintsPhaseB, + e.currentConfig.ValueConstraintsPhaseC, + } + for id := 0; id < len(e.acCurrent); id++ { + if e.powerConfig.SupportsPhases(phases[id]) { + e.acCurrent[id] = measurements.AddDescription(model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypeCurrent), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + Unit: util.Ptr(model.UnitOfMeasurementTypeA), + ScopeType: util.Ptr(model.ScopeTypeTypeACCurrent), + }) + if acCurrentConstraints[id] != nil { + acCurrentConstraints[id].MeasurementId = e.acCurrent[id] + constraints = append(constraints, *acCurrentConstraints[id]) + } + } + } + } + + if e.voltageConfig != nil { + acVoltageConstraints := []*model.MeasurementConstraintsDataType{ + e.voltageConfig.ValueConstraintsPhaseA, + e.voltageConfig.ValueConstraintsPhaseB, + e.voltageConfig.ValueConstraintsPhaseC, + e.voltageConfig.ValueConstraintsPhaseAToB, + e.voltageConfig.ValueConstraintsPhaseBToC, + e.voltageConfig.ValueConstraintsPhaseCToA, + } + for id := 0; id < len(e.acVoltage); id++ { + if e.powerConfig.SupportsPhases(phases[id]) { + if len(phases[id]) == 2 && !e.voltageConfig.SupportPhaseToPhase { + continue + } + e.acVoltage[id] = measurements.AddDescription(model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypeVoltage), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + Unit: util.Ptr(model.UnitOfMeasurementTypeV), + ScopeType: util.Ptr(model.ScopeTypeTypeACVoltage), + }) + if acVoltageConstraints[id] != nil { + acVoltageConstraints[id].MeasurementId = e.acVoltage[id] + constraints = append(constraints, *acVoltageConstraints[id]) + } + } + } + } + + if e.frequencyConfig != nil { + e.acFrequency = measurements.AddDescription(model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypeFrequency), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + Unit: util.Ptr(model.UnitOfMeasurementTypeHz), + ScopeType: util.Ptr(model.ScopeTypeTypeACFrequency), + }) + if e.frequencyConfig.ValueConstraints != nil { + e.frequencyConfig.ValueConstraints.MeasurementId = e.acFrequency + constraints = append(constraints, *e.frequencyConfig.ValueConstraints) + } + } + + electricalConnection, err := server.NewElectricalConnection(e.LocalEntity) + if err != nil { + panic(err) + } + + idEc1 := model.ElectricalConnectionIdType(0) + ec1 := model.ElectricalConnectionDescriptionDataType{ + ElectricalConnectionId: util.Ptr(idEc1), + PowerSupplyType: util.Ptr(model.ElectricalConnectionVoltageTypeTypeAc), + PositiveEnergyDirection: util.Ptr(model.EnergyDirectionTypeConsume), + } + if err := electricalConnection.AddDescription(ec1); err != nil { + panic(err) + } + + p1 := model.ElectricalConnectionParameterDescriptionDataType{ + ElectricalConnectionId: util.Ptr(idEc1), + MeasurementId: e.acPowerTotal, + VoltageType: util.Ptr(model.ElectricalConnectionVoltageTypeTypeAc), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameType(e.powerConfig.ConnectedPhases)), + AcMeasuredInReferenceTo: util.Ptr(model.ElectricalConnectionPhaseNameTypeNeutral), + AcMeasurementType: util.Ptr(model.ElectricalConnectionAcMeasurementTypeTypeReal), + AcMeasurementVariant: util.Ptr(model.ElectricalConnectionMeasurandVariantTypeRms), + } + idP1 := electricalConnection.AddParameterDescription(p1) + if idP1 == nil { + panic("error adding parameter description") + } + + if e.acPower[0] != nil { + p21 := model.ElectricalConnectionParameterDescriptionDataType{ + ElectricalConnectionId: util.Ptr(idEc1), + MeasurementId: e.acPower[0], + VoltageType: util.Ptr(model.ElectricalConnectionVoltageTypeTypeAc), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), + AcMeasuredInReferenceTo: util.Ptr(model.ElectricalConnectionPhaseNameTypeNeutral), + AcMeasurementType: util.Ptr(model.ElectricalConnectionAcMeasurementTypeTypeReal), + AcMeasurementVariant: util.Ptr(model.ElectricalConnectionMeasurandVariantTypeRms), + } + idP21 := electricalConnection.AddParameterDescription(p21) + if idP21 == nil { + panic("error adding parameter description") + } + } + + if e.acPower[1] != nil { + p22 := model.ElectricalConnectionParameterDescriptionDataType{ + ElectricalConnectionId: util.Ptr(idEc1), + MeasurementId: e.acPower[1], + VoltageType: util.Ptr(model.ElectricalConnectionVoltageTypeTypeAc), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeB), + AcMeasuredInReferenceTo: util.Ptr(model.ElectricalConnectionPhaseNameTypeNeutral), + AcMeasurementType: util.Ptr(model.ElectricalConnectionAcMeasurementTypeTypeReal), + AcMeasurementVariant: util.Ptr(model.ElectricalConnectionMeasurandVariantTypeRms), + } + idP22 := electricalConnection.AddParameterDescription(p22) + if idP22 == nil { + panic("error adding parameter description") + } + } + + if e.acPower[2] != nil { + p23 := model.ElectricalConnectionParameterDescriptionDataType{ + ElectricalConnectionId: util.Ptr(idEc1), + MeasurementId: e.acPower[2], + VoltageType: util.Ptr(model.ElectricalConnectionVoltageTypeTypeAc), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeC), + AcMeasuredInReferenceTo: util.Ptr(model.ElectricalConnectionPhaseNameTypeNeutral), + AcMeasurementType: util.Ptr(model.ElectricalConnectionAcMeasurementTypeTypeReal), + AcMeasurementVariant: util.Ptr(model.ElectricalConnectionMeasurandVariantTypeRms), + } + idP23 := electricalConnection.AddParameterDescription(p23) + if idP23 == nil { + panic("error adding parameter description") + } + } + + if e.acEnergyConsumed != nil { + p3 := model.ElectricalConnectionParameterDescriptionDataType{ + ElectricalConnectionId: util.Ptr(idEc1), + MeasurementId: e.acEnergyConsumed, + VoltageType: util.Ptr(model.ElectricalConnectionVoltageTypeTypeAc), + AcMeasurementType: util.Ptr(model.ElectricalConnectionAcMeasurementTypeTypeReal), + } + idP3 := electricalConnection.AddParameterDescription(p3) + if idP3 == nil { + panic("error adding parameter description") + } + } + + if e.acEnergyProduced != nil { + p4 := model.ElectricalConnectionParameterDescriptionDataType{ + ElectricalConnectionId: util.Ptr(idEc1), + MeasurementId: e.acEnergyProduced, + VoltageType: util.Ptr(model.ElectricalConnectionVoltageTypeTypeAc), + AcMeasurementType: util.Ptr(model.ElectricalConnectionAcMeasurementTypeTypeReal), + } + idP4 := electricalConnection.AddParameterDescription(p4) + if idP4 == nil { + panic("error adding parameter description") + } + } + + if e.acCurrent[0] != nil { + p51 := model.ElectricalConnectionParameterDescriptionDataType{ + ElectricalConnectionId: util.Ptr(idEc1), + MeasurementId: e.acCurrent[0], + VoltageType: util.Ptr(model.ElectricalConnectionVoltageTypeTypeAc), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), + AcMeasurementType: util.Ptr(model.ElectricalConnectionAcMeasurementTypeTypeReal), + AcMeasurementVariant: util.Ptr(model.ElectricalConnectionMeasurandVariantTypeRms), + } + idP51 := electricalConnection.AddParameterDescription(p51) + if idP51 == nil { + panic("error adding parameter description") + } + } + + if e.acCurrent[1] != nil { + p52 := model.ElectricalConnectionParameterDescriptionDataType{ + ElectricalConnectionId: util.Ptr(idEc1), + MeasurementId: e.acCurrent[1], + VoltageType: util.Ptr(model.ElectricalConnectionVoltageTypeTypeAc), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeB), + AcMeasurementType: util.Ptr(model.ElectricalConnectionAcMeasurementTypeTypeReal), + AcMeasurementVariant: util.Ptr(model.ElectricalConnectionMeasurandVariantTypeRms), + } + idP52 := electricalConnection.AddParameterDescription(p52) + if idP52 == nil { + panic("error adding parameter description") + } + } + + if e.acCurrent[2] != nil { + p53 := model.ElectricalConnectionParameterDescriptionDataType{ + ElectricalConnectionId: util.Ptr(idEc1), + MeasurementId: e.acCurrent[2], + VoltageType: util.Ptr(model.ElectricalConnectionVoltageTypeTypeAc), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeC), + AcMeasurementType: util.Ptr(model.ElectricalConnectionAcMeasurementTypeTypeReal), + AcMeasurementVariant: util.Ptr(model.ElectricalConnectionMeasurandVariantTypeRms), + } + idP53 := electricalConnection.AddParameterDescription(p53) + if idP53 == nil { + panic("error adding parameter description") + } + } + + if e.acVoltage[0] != nil { + p61 := model.ElectricalConnectionParameterDescriptionDataType{ + ElectricalConnectionId: util.Ptr(idEc1), + MeasurementId: e.acVoltage[0], + VoltageType: util.Ptr(model.ElectricalConnectionVoltageTypeTypeAc), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), + AcMeasuredInReferenceTo: util.Ptr(model.ElectricalConnectionPhaseNameTypeNeutral), + AcMeasurementType: util.Ptr(model.ElectricalConnectionAcMeasurementTypeTypeApparent), + AcMeasurementVariant: util.Ptr(model.ElectricalConnectionMeasurandVariantTypeRms), + } + idP61 := electricalConnection.AddParameterDescription(p61) + if idP61 == nil { + panic("error adding parameter description") + } + } + + if e.acVoltage[1] != nil { + p62 := model.ElectricalConnectionParameterDescriptionDataType{ + ElectricalConnectionId: util.Ptr(idEc1), + MeasurementId: e.acVoltage[1], + VoltageType: util.Ptr(model.ElectricalConnectionVoltageTypeTypeAc), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeB), + AcMeasuredInReferenceTo: util.Ptr(model.ElectricalConnectionPhaseNameTypeNeutral), + AcMeasurementType: util.Ptr(model.ElectricalConnectionAcMeasurementTypeTypeApparent), + AcMeasurementVariant: util.Ptr(model.ElectricalConnectionMeasurandVariantTypeRms), + } + idP62 := electricalConnection.AddParameterDescription(p62) + if idP62 == nil { + panic("error adding parameter description") + } + } + + if e.acVoltage[2] != nil { + p63 := model.ElectricalConnectionParameterDescriptionDataType{ + ElectricalConnectionId: util.Ptr(idEc1), + MeasurementId: e.acVoltage[2], + VoltageType: util.Ptr(model.ElectricalConnectionVoltageTypeTypeAc), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeC), + AcMeasuredInReferenceTo: util.Ptr(model.ElectricalConnectionPhaseNameTypeNeutral), + AcMeasurementType: util.Ptr(model.ElectricalConnectionAcMeasurementTypeTypeApparent), + AcMeasurementVariant: util.Ptr(model.ElectricalConnectionMeasurandVariantTypeRms), + } + idP63 := electricalConnection.AddParameterDescription(p63) + if idP63 == nil { + panic("error adding parameter description") + } + } + + if e.acVoltage[3] != nil { + p64 := model.ElectricalConnectionParameterDescriptionDataType{ + ElectricalConnectionId: util.Ptr(idEc1), + MeasurementId: e.acVoltage[3], + VoltageType: util.Ptr(model.ElectricalConnectionVoltageTypeTypeAc), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), + AcMeasuredInReferenceTo: util.Ptr(model.ElectricalConnectionPhaseNameTypeB), + AcMeasurementType: util.Ptr(model.ElectricalConnectionAcMeasurementTypeTypeApparent), + AcMeasurementVariant: util.Ptr(model.ElectricalConnectionMeasurandVariantTypeRms), + } + idP64 := electricalConnection.AddParameterDescription(p64) + if idP64 == nil { + panic("error adding parameter description") + } + } + + if e.acVoltage[4] != nil { + p65 := model.ElectricalConnectionParameterDescriptionDataType{ + ElectricalConnectionId: util.Ptr(idEc1), + MeasurementId: e.acVoltage[4], + VoltageType: util.Ptr(model.ElectricalConnectionVoltageTypeTypeAc), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeB), + AcMeasuredInReferenceTo: util.Ptr(model.ElectricalConnectionPhaseNameTypeC), + AcMeasurementType: util.Ptr(model.ElectricalConnectionAcMeasurementTypeTypeApparent), + AcMeasurementVariant: util.Ptr(model.ElectricalConnectionMeasurandVariantTypeRms), + } + idP65 := electricalConnection.AddParameterDescription(p65) + if idP65 == nil { + panic("error adding parameter description") + } + } + + if e.acVoltage[5] != nil { + p66 := model.ElectricalConnectionParameterDescriptionDataType{ + ElectricalConnectionId: util.Ptr(idEc1), + MeasurementId: e.acVoltage[5], + VoltageType: util.Ptr(model.ElectricalConnectionVoltageTypeTypeAc), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeC), + AcMeasuredInReferenceTo: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), + AcMeasurementType: util.Ptr(model.ElectricalConnectionAcMeasurementTypeTypeApparent), + AcMeasurementVariant: util.Ptr(model.ElectricalConnectionMeasurandVariantTypeRms), + } + idP66 := electricalConnection.AddParameterDescription(p66) + if idP66 == nil { + panic("error adding parameter description") + } + } + + if e.acFrequency != nil { + p7 := model.ElectricalConnectionParameterDescriptionDataType{ + ElectricalConnectionId: util.Ptr(idEc1), + MeasurementId: e.acFrequency, + VoltageType: util.Ptr(model.ElectricalConnectionVoltageTypeTypeAc), + } + idP7 := electricalConnection.AddParameterDescription(p7) + if idP7 == nil { + panic("error adding parameter description") + } + } + + if len(constraints) > 0 { + measurementFeature.UpdateData( + model.FunctionTypeMeasurementConstraintsListData, + &model.MeasurementConstraintsListDataType{ + MeasurementConstraintsData: constraints, + }, nil, nil, + ) + } +} + +func (e *MPC) getMeasurementDataForId(id *model.MeasurementIdType) (float64, error) { + measurements, err := server.NewMeasurement(e.LocalEntity) + if err != nil { + return 0, err + } + + data, err := measurements.GetDataForId(*id) + if err != nil { + return 0, err + } + + if data == nil { + return 0, api.ErrDataNotAvailable + } + + return data.Value.GetValue(), nil +} + +func measurementData( + value float64, + timestamp *time.Time, + valueSource *model.MeasurementValueSourceType, + valueState *model.MeasurementValueStateType, + evaluationStart *time.Time, + evaluationEnd *time.Time, +) model.MeasurementDataType { + measurement := model.MeasurementDataType{ + ValueType: util.Ptr(model.MeasurementValueTypeTypeValue), + Value: model.NewScaledNumberType(value), + ValueSource: valueSource, + ValueState: valueState, + } + + if timestamp != nil { + measurement.Timestamp = model.NewAbsoluteOrRelativeTimeTypeFromTime(*timestamp) + } + + if evaluationStart != nil && evaluationEnd != nil { + measurement.EvaluationPeriod = &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeTypeFromTime(*evaluationStart), + EndTime: model.NewAbsoluteOrRelativeTimeTypeFromTime(*evaluationEnd), + } + } + + return measurement +} diff --git a/usecases/mu/mpc/usecase_test.go b/usecases/mu/mpc/usecase_test.go new file mode 100644 index 00000000..af42d9f2 --- /dev/null +++ b/usecases/mu/mpc/usecase_test.go @@ -0,0 +1,151 @@ +package mpc + +import ( + "testing" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + shipapi "github.com/enbility/ship-go/api" + "github.com/enbility/ship-go/cert" + spineapi "github.com/enbility/spine-go/api" + spinemocks "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestBasicSuite(t *testing.T) { + suite.Run(t, new(BasicSuite)) +} + +type BasicSuite struct { + suite.Suite + + service api.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *spinemocks.EntityRemoteInterface + monitoredEntity spineapi.EntityRemoteInterface + loadControlFeature, + deviceDiagnosisFeature, + deviceConfigurationFeature spineapi.FeatureLocalInterface + + eventCalled bool +} + +func (s *BasicSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + s.eventCalled = true +} + +func (s *BasicSuite) BeforeTest(suiteName, testName string) { + s.eventCalled = false + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := api.NewConfiguration( + "test", "test", "test", "test", + []shipapi.DeviceCategoryType{shipapi.DeviceCategoryTypeEnergyManagementSystem}, + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeInverter}, + 9999, cert, time.Second*4) + + serviceHandler := mocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := spinemocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = spinemocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := spinemocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() +} + +func (s *BasicSuite) Test_MpcOptionalParameters() { + localEntity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeInverter) + + // required + var monitorPowerConfig = MonitorPowerConfig{ + ConnectedPhases: ConnectedPhasesABC, + ValueSourceTotal: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseA: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseB: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseC: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + } + + // the following 4 parameters are optional and can be nil + var monitorEnergyConfig = MonitorEnergyConfig{ + ValueSourceProduction: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourceConsumption: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + } + var monitorCurrentConfig = MonitorCurrentConfig{ + ValueSourcePhaseA: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseB: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseC: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + } + var monitorVoltageConfig = MonitorVoltageConfig{ + SupportPhaseToPhase: true, + ValueSourcePhaseA: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseB: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseC: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseAToB: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseBToC: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseCToA: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + } + var monitorFrequencyConfig = MonitorFrequencyConfig{ + ValueSource: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueConstraints: util.Ptr(model.MeasurementConstraintsDataType{ + ValueRangeMin: model.NewScaledNumberType(0), + ValueRangeMax: model.NewScaledNumberType(100), + ValueStepSize: model.NewScaledNumberType(1), + }), + } + + numOptionalParams := 4 + + // iterate over all permutations of nil/set + for i := 0; i < (1 << numOptionalParams); i++ { + // Determine which parameters to set + var optEnergyConfig *MonitorEnergyConfig + var optCurrentConfig *MonitorCurrentConfig + var optVoltageConfig *MonitorVoltageConfig + var optFrequencyConfig *MonitorFrequencyConfig + if i&1 != 0 { + optEnergyConfig = &monitorEnergyConfig + } + if i&2 != 0 { + optCurrentConfig = &monitorCurrentConfig + } + if i&4 != 0 { + optVoltageConfig = &monitorVoltageConfig + } + if i&8 != 0 { + optFrequencyConfig = &monitorFrequencyConfig + } + + mpc, err := NewMPC( + localEntity, + s.Event, + &monitorPowerConfig, + optEnergyConfig, + optCurrentConfig, + optVoltageConfig, + optFrequencyConfig, + ) + + assert.Nil(s.T(), err) + + mpc.AddFeatures() + mpc.AddUseCase() + } +} diff --git a/usecases/usecase/events.go b/usecases/usecase/events.go index 0bab5b49..a879f8ed 100644 --- a/usecases/usecase/events.go +++ b/usecases/usecase/events.go @@ -81,7 +81,7 @@ func (u *UseCaseBase) useCaseDataUpdate( } for _, entity := range entitiesToCheck { - if !slices.Contains(u.validEntityTypes, entity.EntityType()) { + if !u.allEntityTypesValid && !slices.Contains(u.validEntityTypes, entity.EntityType()) { continue } diff --git a/usecases/usecase/testhelper_test.go b/usecases/usecase/testhelper_test.go index 77df868c..6ecaf71f 100644 --- a/usecases/usecase/testhelper_test.go +++ b/usecases/usecase/testhelper_test.go @@ -110,6 +110,7 @@ func (s *UseCaseSuite) BeforeTest(suiteName, testName string) { useCaseUpdateEvent, validActorTypes, validEntityTypes, + false, ) } diff --git a/usecases/usecase/usecase.go b/usecases/usecase/usecase.go index 863f7b05..7968c94d 100644 --- a/usecases/usecase/usecase.go +++ b/usecases/usecase/usecase.go @@ -25,8 +25,9 @@ type UseCaseBase struct { availableEntityScenarios []api.RemoteEntityScenarios // map of scenarios and their availability for each compatible remote entity - validActorTypes []model.UseCaseActorType // valid remote actor types for this use case - validEntityTypes []model.EntityTypeType // valid remote entity types for this use case + validActorTypes []model.UseCaseActorType // valid remote actor types for this use case + validEntityTypes []model.EntityTypeType // valid remote entity types for this use case + allEntityTypesValid bool mux sync.Mutex } @@ -44,6 +45,7 @@ func NewUseCaseBase( useCaseUpdateEvent api.EventType, validActorTypes []model.UseCaseActorType, validEntityTypes []model.EntityTypeType, + allEntityTypesValid bool, ) *UseCaseBase { ucb := &UseCaseBase{ LocalEntity: localEntity, @@ -56,6 +58,7 @@ func NewUseCaseBase( useCaseUpdateEvent: useCaseUpdateEvent, validActorTypes: validActorTypes, validEntityTypes: validEntityTypes, + allEntityTypesValid: allEntityTypesValid, } _ = spine.Events.Subscribe(ucb) @@ -91,6 +94,10 @@ func (u *UseCaseBase) IsCompatibleEntityType(entity spineapi.EntityRemoteInterfa return false } + if u.allEntityTypesValid { + return true + } + return slices.Contains(u.validEntityTypes, entity.EntityType()) }