diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index ff47919..86c198c 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -34,3 +34,8 @@ jobs: - name: Test run: go test -race -v -coverprofile=coverage.out -covermode=atomic ./... + + - name: Send coverage + uses: shogo82148/actions-goveralls@v1 + with: + path-to-profile: coverage.out \ No newline at end of file diff --git a/README.md b/README.md index 17e58e2..ea03fc3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # cemd -[![Build Status](https://github.com/enbility/cemd/actions/workflows/default.yml/badge.svg?branch=main)](https://github.com/enbility/cemd/actions/workflows/default.yml/badge.svg?branch=main) +[![Build Status](https://github.com/enbility/cemd/actions/workflows/default.yml/badge.svg?branch=dev)](https://github.com/enbility/cemd/actions/workflows/default.yml/badge.svg?branch=dev) [![GoDoc](https://img.shields.io/badge/godoc-reference-5272B4)](https://godoc.org/github.com/enbility/cemd) +[![Coverage Status](https://coveralls.io/repos/github/enbility/cemd/badge.svg?branch=dev)](https://coveralls.io/github/enbility/cemd?branch=dev) [![Go report](https://goreportcard.com/badge/github.com/enbility/cemd)](https://goreportcard.com/report/github.com/enbility/cemd) The goal is to provide an EEBUS CEM implementation diff --git a/cem/cem.go b/cem/cem.go index 99a9468..e7d70bc 100644 --- a/cem/cem.go +++ b/cem/cem.go @@ -2,20 +2,29 @@ package cem import ( "github.com/enbility/cemd/emobility" + "github.com/enbility/cemd/grid" + "github.com/enbility/cemd/inverterbatteryvis" + "github.com/enbility/cemd/inverterpvvis" + "github.com/enbility/cemd/scenarios" "github.com/enbility/eebus-go/logging" "github.com/enbility/eebus-go/service" "github.com/enbility/eebus-go/spine" + "github.com/enbility/eebus-go/spine/model" ) // Generic CEM implementation type CemImpl struct { - service *service.EEBUSService - emobilityScenario *emobility.EmobilityScenarioImpl + service *service.EEBUSService + + emobilityScenario, gridScenario, inverterBatteryVisScenario, inverterPVVisScenario scenarios.ScenariosI + + Currency model.CurrencyType } func NewCEM(serviceDescription *service.Configuration, serviceHandler service.EEBUSServiceHandler, log logging.Logging) *CemImpl { cem := &CemImpl{ - service: service.NewEEBUSService(serviceDescription, serviceHandler), + service: service.NewEEBUSService(serviceDescription, serviceHandler), + Currency: model.CurrencyTypeEur, } cem.service.SetLogging(log) @@ -24,23 +33,42 @@ func NewCEM(serviceDescription *service.Configuration, serviceHandler service.EE } // Set up the supported usecases and features -func (h *CemImpl) Setup(enableEmobility bool) error { +func (h *CemImpl) Setup() error { if err := h.service.Setup(); err != nil { return err } spine.Events.Subscribe(h) - // Setup the supported usecases and features - if enableEmobility { - h.emobilityScenario = emobility.NewEMobilityScenario(h.service) - h.emobilityScenario.AddFeatures() - h.emobilityScenario.AddUseCases() - } - return nil } +// Enable the supported usecases and features + +func (h *CemImpl) EnableEmobility(configuration emobility.EmobilityConfiguration) { + h.emobilityScenario = emobility.NewEMobilityScenario(h.service, h.Currency, configuration) + h.emobilityScenario.AddFeatures() + h.emobilityScenario.AddUseCases() +} + +func (h *CemImpl) EnableGrid() { + h.gridScenario = grid.NewGridScenario(h.service) + h.gridScenario.AddFeatures() + h.gridScenario.AddUseCases() +} + +func (h *CemImpl) EnableBatteryVisualization() { + h.inverterBatteryVisScenario = inverterbatteryvis.NewInverterVisScenario(h.service) + h.inverterBatteryVisScenario.AddFeatures() + h.inverterBatteryVisScenario.AddUseCases() +} + +func (h *CemImpl) EnablePVVisualization() { + h.inverterPVVisScenario = inverterpvvis.NewInverterVisScenario(h.service) + h.inverterPVVisScenario.AddFeatures() + h.inverterPVVisScenario.AddUseCases() +} + func (h *CemImpl) Start() { h.service.Start() } @@ -49,10 +77,45 @@ func (h *CemImpl) Shutdown() { h.service.Shutdown() } -func (h *CemImpl) RegisterEmobilityRemoteDevice(details *service.ServiceDetails) *emobility.EMobilityImpl { - return h.emobilityScenario.RegisterEmobilityRemoteDevice(details) +func (h *CemImpl) RegisterEmobilityRemoteDevice(details *service.ServiceDetails, dataProvider emobility.EmobilityDataProvider) *emobility.EMobilityImpl { + var impl any + + if dataProvider != nil { + impl = h.emobilityScenario.RegisterRemoteDevice(details, dataProvider) + } else { + impl = h.emobilityScenario.RegisterRemoteDevice(details, nil) + } + + return impl.(*emobility.EMobilityImpl) } func (h *CemImpl) UnRegisterEmobilityRemoteDevice(remoteDeviceSki string) error { - return h.emobilityScenario.UnRegisterEmobilityRemoteDevice(remoteDeviceSki) + return h.emobilityScenario.UnRegisterRemoteDevice(remoteDeviceSki) +} + +func (h *CemImpl) RegisterGridRemoteDevice(details *service.ServiceDetails) *grid.GridImpl { + impl := h.gridScenario.RegisterRemoteDevice(details, nil) + return impl.(*grid.GridImpl) +} + +func (h *CemImpl) UnRegisterGridRemoteDevice(remoteDeviceSki string) error { + return h.gridScenario.UnRegisterRemoteDevice(remoteDeviceSki) +} + +func (h *CemImpl) RegisterInverterBatteryVisRemoteDevice(details *service.ServiceDetails) *grid.GridImpl { + impl := h.inverterBatteryVisScenario.RegisterRemoteDevice(details, nil) + return impl.(*grid.GridImpl) +} + +func (h *CemImpl) UnRegisterInverterBatteryVisRemoteDevice(remoteDeviceSki string) error { + return h.inverterBatteryVisScenario.UnRegisterRemoteDevice(remoteDeviceSki) +} + +func (h *CemImpl) RegisterInverterPVVisRemoteDevice(details *service.ServiceDetails) *grid.GridImpl { + impl := h.inverterPVVisScenario.RegisterRemoteDevice(details, nil) + return impl.(*grid.GridImpl) +} + +func (h *CemImpl) UnRegisterInverterPVVisRemoteDevice(remoteDeviceSki string) error { + return h.inverterPVVisScenario.UnRegisterRemoteDevice(remoteDeviceSki) } diff --git a/cmd/main.go b/cmd/main.go index d82c23e..982a61e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -10,6 +10,7 @@ import ( "time" "github.com/enbility/cemd/cem" + "github.com/enbility/cemd/emobility" "github.com/enbility/eebus-go/logging" "github.com/enbility/eebus-go/service" "github.com/enbility/eebus-go/spine/model" @@ -28,7 +29,18 @@ func NewDemoCem(configuration *service.Configuration) *DemoCem { } func (d *DemoCem) Setup() error { - return d.cem.Setup(true) + if err := d.cem.Setup(); err != nil { + return err + } + + d.cem.EnableEmobility(emobility.EmobilityConfiguration{ + CoordinatedChargingEnabled: true, + }) + d.cem.EnableGrid() + d.cem.EnableBatteryVisualization() + d.cem.EnablePVVisualization() + + return nil } // report the Ship ID of a newly trusted connection @@ -135,7 +147,7 @@ func main() { } remoteService := service.NewServiceDetails(os.Args[2]) - demo.cem.RegisterEmobilityRemoteDevice(remoteService) + demo.cem.RegisterEmobilityRemoteDevice(remoteService, nil) // Clean exit to make sure mdns shutdown is invoked sig := make(chan os.Signal, 1) diff --git a/emobility/emobility.go b/emobility/emobility.go index 68c7477..a19966f 100644 --- a/emobility/emobility.go +++ b/emobility/emobility.go @@ -4,9 +4,40 @@ import ( "github.com/enbility/eebus-go/features" "github.com/enbility/eebus-go/service" "github.com/enbility/eebus-go/spine" + "github.com/enbility/eebus-go/spine/model" "github.com/enbility/eebus-go/util" ) +// used by emobility and implemented by the CEM +type EmobilityDataProvider interface { + // The EV provided a charge strategy + EVProvidedChargeStrategy(strategy EVChargeStrategyType) + + // Energy demand and duration is provided by the EV which requires the CEM + // to respond with time slots containing power limits for each slot + // + // `EVWritePowerLimits` must be invoked within <55s, idealy <15s, after receiving this call + // + // Parameters: + // - demand: Contains details about the actual demands from the EV + // - constraints: Contains details about the time slot constraints + EVRequestPowerLimits(demand EVDemand, constraints EVTimeSlotConstraints) + + // Energy demand and duration is provided by the EV which requires the CEM + // to respond with time slots containing incentives for each slot + // + // `EVWriteIncentives` must be invoked within <20s after receiving this call + // + // Parameters: + // - demand: Contains details about the actual demands from the EV + // - constraints: Contains details about the incentive slot constraints + EVRequestIncentives(demand EVDemand, constraints EVIncentiveSlotConstraints) + + // The EV provided a charge plan + EVProvidedChargePlan(data []EVDurationSlotValue) +} + +// used by the CEM and implemented by emobility type EmobilityI interface { // return the current charge sate of the EV EVCurrentChargeState() (EVChargeStateType, error) @@ -42,6 +73,13 @@ type EmobilityI interface { // - and others EVCurrentLimits() ([]float64, []float64, []float64, error) + // return the current loadcontrol obligation limits + // + // possible errors: + // - ErrDataNotAvailable if no such measurement is (yet) available + // - and others + EVLoadControlObligationLimits() ([]float64, error) + // send new LoadControlLimits to the remote EV // // parameters: @@ -131,6 +169,44 @@ type EmobilityI interface { // - ErrDataNotAvailable if that information is not (yet) available // - and others EVCoordinatedChargingSupported() (bool, error) + + // returns the current charging stratey + // + // returns EVChargeStrategyTypeUnknown if it could not be determined, e.g. + // if the vehicle communication is via IEC61851 or the EV doesn't provide + // any information about its charging mode or plan + EVChargeStrategy() EVChargeStrategyType + + // returns the current energy demand + // - EVDemand: details about the actual demands from the EV + // - error: if no data is available + // + // if duration is 0, direct charging is active, otherwise timed charging is active + EVEnergyDemand() (EVDemand, error) + + // returns the constraints for the power slots + // - EVTimeSlotConstraints: details about the time slot constraints + EVGetPowerConstraints() EVTimeSlotConstraints + + // send power limits data to the EV + // + // returns an error if sending failed or charge slot count do not meet requirements + // + // this needs to be invoked either <55s, idealy <15s, of receiving a call to EVRequestPowerLimits + // or if the CEM requires the EV to change its charge plan + EVWritePowerLimits(data []EVDurationSlotValue) error + + // returns the constraints for incentive slots + // - EVIncentiveConstraints: details about the incentive slot constraints + EVGetIncentiveConstraints() EVIncentiveSlotConstraints + + // send price slots data to the EV + // + // returns an error if sending failed or charge slot count do not meet requirements + // + // this needs to be invoked either within 20s of receiving a call to EVRequestIncentives + // or if the CEM requires the EV to change its charge plan + EVWriteIncentives(data []EVDurationSlotValue) error } type EMobilityImpl struct { @@ -151,20 +227,32 @@ type EMobilityImpl struct { evMeasurement *features.Measurement evIdentification *features.Identification evLoadControl *features.LoadControl + evTimeSeries *features.TimeSeries + evIncentiveTable *features.IncentiveTable + + evCurrentChargeStrategy EVChargeStrategyType + + ski string + currency model.CurrencyType - ski string + configuration EmobilityConfiguration + dataProvider EmobilityDataProvider } var _ EmobilityI = (*EMobilityImpl)(nil) // Add E-Mobility support -func NewEMobility(service *service.EEBUSService, details *service.ServiceDetails) *EMobilityImpl { +func NewEMobility(service *service.EEBUSService, details *service.ServiceDetails, currency model.CurrencyType, configuration EmobilityConfiguration, dataProvider EmobilityDataProvider) *EMobilityImpl { ski := util.NormalizeSKI(details.SKI()) emobility := &EMobilityImpl{ - service: service, - entity: service.LocalEntity(), - ski: ski, + service: service, + entity: service.LocalEntity(), + ski: ski, + currency: currency, + dataProvider: dataProvider, + evCurrentChargeStrategy: EVChargeStrategyTypeUnknown, + configuration: configuration, } spine.Events.Subscribe(emobility) diff --git a/emobility/evcoordinatedcharging_test.go b/emobility/evcoordinatedcharging_test.go new file mode 100644 index 0000000..a2f8c0b --- /dev/null +++ b/emobility/evcoordinatedcharging_test.go @@ -0,0 +1,404 @@ +package emobility + +import ( + "testing" + "time" + + "github.com/enbility/eebus-go/spine" + "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/eebus-go/util" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" +) + +func Test_CoordinatedChargingScenarios(t *testing.T) { + emobility, eebusService := setupEmobility() + + data, err := emobility.EVChargedEnergy() + assert.NotNil(t, err) + assert.Equal(t, 0.0, data) + + localDevice, remoteDevice, entites, _ := setupDevices(eebusService) + emobility.evseEntity = entites[0] + emobility.evEntity = entites[1] + + ctrl := gomock.NewController(t) + + dataProviderMock := NewMockEmobilityDataProvider(ctrl) + emobility.dataProvider = dataProviderMock + + emobility.evTimeSeries = timeSeriesConfiguration(localDevice, emobility.evEntity) + emobility.evIncentiveTable = incentiveTableConfiguration(localDevice, emobility.evEntity) + + datagramtt := datagramForEntityAndFeatures(false, localDevice, emobility.evEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeServer, model.RoleTypeClient) + datagramit := datagramForEntityAndFeatures(false, localDevice, emobility.evEntity, model.FeatureTypeTypeIncentiveTable, model.RoleTypeServer, model.RoleTypeClient) + + setupTimeSeries(t, datagramtt, localDevice, remoteDevice) + setupIncentiveTable(t, datagramit, localDevice, remoteDevice) + + // demand, No Profile No Timer demand + + cmd := []model.CmdType{{ + TimeSeriesListData: &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(3)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(1)), + Value: model.NewScaledNumberType(0), + MaxValue: model.NewScaledNumberType(74690), + }, + }, + }, + }, + }, + }} + + datagramtt.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagramtt, remoteDevice) + assert.Nil(t, err) + + demand, err := emobility.EVEnergyDemand() + assert.Nil(t, err) + assert.Equal(t, 0.0, demand.MinDemand) + assert.Equal(t, 0.0, demand.OptDemand) + assert.Equal(t, 74690.0, demand.MaxDemand) + assert.Equal(t, time.Duration(0), demand.DurationUntilStart) + assert.Equal(t, time.Duration(0), demand.DurationUntilEnd) + + // the final plan + + cmd = []model.CmdType{{ + TimeSeriesListData: &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(2)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + Duration: util.Ptr(model.DurationType("PT18H3M7S")), + MaxValue: model.NewScaledNumberType(4163), + }, + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(1)), + Duration: util.Ptr(model.DurationType("PT42M")), + MaxValue: model.NewScaledNumberType(2736), + }, + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(1)), + Duration: util.Ptr(model.DurationType("P1D")), + MaxValue: model.NewScaledNumberType(0), + }, + }, + }, + }, + }, + }} + + datagramtt.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagramtt, remoteDevice) + assert.Nil(t, err) + + // demand, profile + timer with 80% target and no climate, minSoC reached + + cmd = []model.CmdType{{ + TimeSeriesListData: &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(3)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(1)), + Duration: util.Ptr(model.DurationType("P2DT4H40M36S")), + Value: model.NewScaledNumberType(53400), + MaxValue: model.NewScaledNumberType(74690), + }, + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(1)), + Duration: util.Ptr(model.DurationType("P1D")), + MaxValue: model.NewScaledNumberType(0), + }, + }, + }, + }, + }, + }} + + datagramtt.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagramtt, remoteDevice) + assert.Nil(t, err) + + demand, err = emobility.EVEnergyDemand() + assert.Nil(t, err) + assert.Equal(t, 0.0, demand.MinDemand) + assert.Equal(t, 53400.0, demand.OptDemand) + assert.Equal(t, 74690.0, demand.MaxDemand) + assert.Equal(t, time.Duration(0), demand.DurationUntilStart) + assert.Equal(t, time.Duration(time.Hour*52+time.Minute*40+time.Second*36), demand.DurationUntilEnd) + + // the final plan + + cmd = []model.CmdType{{ + TimeSeriesListData: &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(2)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + Duration: util.Ptr(model.DurationType("P1DT15H24M24S")), + MaxValue: model.NewScaledNumberType(0), + }, + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(1)), + Duration: util.Ptr(model.DurationType("PT12H35M50S")), + MaxValue: model.NewScaledNumberType(4163), + }, + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(2)), + Duration: util.Ptr(model.DurationType("PT40M22S")), + MaxValue: model.NewScaledNumberType(0), + }, + }, + }, + }, + }, + }} + + datagramtt.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagramtt, remoteDevice) + assert.Nil(t, err) + + // demand, profile with 25% min SoC, minSoC not reached, no timer + + cmd = []model.CmdType{{ + TimeSeriesListData: &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(1)), + }, + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(2)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + Duration: util.Ptr(model.DurationType("PT8M42S")), + MaxValue: model.NewScaledNumberType(4212), + }, + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(1)), + Duration: util.Ptr(model.DurationType("P1D")), + MaxValue: model.NewScaledNumberType(0), + }, + }, + }, + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(3)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(1)), + Value: model.NewScaledNumberType(600), + MinValue: model.NewScaledNumberType(600), + MaxValue: model.NewScaledNumberType(75600), + }, + }, + }, + }, + }, + }} + + datagramtt.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagramtt, remoteDevice) + assert.Nil(t, err) + + demand, err = emobility.EVEnergyDemand() + assert.Nil(t, err) + assert.Equal(t, 600.0, demand.MinDemand) + assert.Equal(t, 600.0, demand.OptDemand) + assert.Equal(t, 75600.0, demand.MaxDemand) + assert.Equal(t, time.Duration(0), demand.DurationUntilStart) + assert.Equal(t, time.Duration(0), demand.DurationUntilEnd) + + // the final plan + + cmd = []model.CmdType{{ + TimeSeriesListData: &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(2)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + Duration: util.Ptr(model.DurationType("PT8M42S")), + MaxValue: model.NewScaledNumberType(4212), + }, + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(1)), + Duration: util.Ptr(model.DurationType("P1D")), + MaxValue: model.NewScaledNumberType(0), + }, + }, + }, + }, + }, + }} + + datagramtt.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagramtt, remoteDevice) + assert.Nil(t, err) +} + +func setupTimeSeries(t *testing.T, datagram model.DatagramType, localDevice *spine.DeviceLocalImpl, remoteDevice *spine.DeviceRemoteImpl) { + cmd := []model.CmdType{{ + TimeSeriesConstraintsListData: &model.TimeSeriesConstraintsListDataType{ + TimeSeriesConstraintsData: []model.TimeSeriesConstraintsDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(1)), + SlotCountMax: util.Ptr(model.TimeSeriesSlotCountType(30)), + }, + }, + }, + }} + + datagram.Payload.Cmd = cmd + + err := localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + cmd = []model.CmdType{{ + TimeSeriesDescriptionListData: &model.TimeSeriesDescriptionListDataType{ + TimeSeriesDescriptionData: []model.TimeSeriesDescriptionDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(1)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeConstraints), + TimeSeriesWriteable: util.Ptr(true), + UpdateRequired: util.Ptr(false), + Unit: util.Ptr(model.UnitOfMeasurementTypeW), + }, + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(2)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypePlan), + TimeSeriesWriteable: util.Ptr(false), + Unit: util.Ptr(model.UnitOfMeasurementTypeW), + }, + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(3)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeSingleDemand), + TimeSeriesWriteable: util.Ptr(false), + Unit: util.Ptr(model.UnitOfMeasurementTypeWh), + }, + }, + }, + }} + + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) +} + +func setupIncentiveTable(t *testing.T, datagram model.DatagramType, localDevice *spine.DeviceLocalImpl, remoteDevice *spine.DeviceRemoteImpl) { + cmd := []model.CmdType{{ + IncentiveTableDescriptionData: &model.IncentiveTableDescriptionDataType{ + IncentiveTableDescription: []model.IncentiveTableDescriptionType{ + { + TariffDescription: &model.TariffDescriptionDataType{ + TariffId: util.Ptr(model.TariffIdType(1)), + TariffWriteable: util.Ptr(true), + UpdateRequired: util.Ptr(false), + ScopeType: util.Ptr(model.ScopeTypeTypeSimpleIncentiveTable), + }, + }, + }, + }, + }} + + datagram.Payload.Cmd = cmd + + err := localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) +} + +/* +func requestIncentiveUpdate(t *testing.T, datagram model.DatagramType, localDevice *spine.DeviceLocalImpl, remoteDevice *spine.DeviceRemoteImpl) { + cmd := []model.CmdType{{ + IncentiveTableDescriptionData: &model.IncentiveTableDescriptionDataType{ + IncentiveTableDescription: []model.IncentiveTableDescriptionType{ + { + TariffDescription: &model.TariffDescriptionDataType{ + TariffId: util.Ptr(model.TariffIdType(1)), + TariffWriteable: util.Ptr(true), + UpdateRequired: util.Ptr(true), + ScopeType: util.Ptr(model.ScopeTypeTypeSimpleIncentiveTable), + }, + }, + }, + }, + }} + + datagram.Payload.Cmd = cmd + + err := localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) +} + +func requestPowerTableUpdate(t *testing.T, datagram model.DatagramType, localDevice *spine.DeviceLocalImpl, remoteDevice *spine.DeviceRemoteImpl) { + cmd := []model.CmdType{{ + TimeSeriesDescriptionListData: &model.TimeSeriesDescriptionListDataType{ + TimeSeriesDescriptionData: []model.TimeSeriesDescriptionDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(1)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeConstraints), + TimeSeriesWriteable: util.Ptr(true), + UpdateRequired: util.Ptr(true), + }, + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(2)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypePlan), + TimeSeriesWriteable: util.Ptr(false), + Unit: util.Ptr(model.UnitOfMeasurementTypeW), + }, + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(3)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeConstraints), + TimeSeriesWriteable: util.Ptr(false), + Unit: util.Ptr(model.UnitOfMeasurementTypeWh), + }, + }, + }, + }} + + datagram.Payload.Cmd = cmd + + err := localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) +} +*/ diff --git a/emobility/events.go b/emobility/events.go index 2107373..bd7ba69 100644 --- a/emobility/events.go +++ b/emobility/events.go @@ -5,16 +5,17 @@ import ( "github.com/enbility/eebus-go/logging" "github.com/enbility/eebus-go/spine" "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/eebus-go/util" ) // Internal EventHandler Interface for the CEM func (e *EMobilityImpl) HandleEvent(payload spine.EventPayload) { - // we only care about the registered SKI + // only care about the registered SKI if payload.Ski != e.ski { return } - // we care only about events for this remote device + // only care about events for this remote device if payload.Device != nil && payload.Device.Ski() != e.ski { return } @@ -61,135 +62,223 @@ func (e *EMobilityImpl) HandleEvent(payload spine.EventPayload) { case spine.EventTypeDataChange: if payload.ChangeType == spine.ElementChangeUpdate { switch payload.Data.(type) { - /* - case *model.DeviceClassificationManufacturerDataType: - var feature *features.DeviceClassification - if entityType == model.EntityTypeTypeEVSE { - feature = e.evseDeviceClassification - } else { - feature = e.evDeviceClassification - } - _, err := feature.GetManufacturerDetails() - if err != nil { - logging.Log.Error("Error getting manufacturer data:", err) - return - } - - // TODO: provide the current data to the CEM - */ case *model.DeviceConfigurationKeyValueDescriptionListDataType: + if e.evDeviceConfiguration == nil { + break + } + // key value descriptions received, now get the data - _, err := e.evDeviceConfiguration.RequestKeyValueList() - if err != nil { + if _, err := e.evDeviceConfiguration.RequestKeyValues(); err != nil { logging.Log.Error("Error getting configuration key values:", err) } - /* - case *model.DeviceConfigurationKeyValueListDataType: - data, err := e.evDeviceConfiguration.GetValues() - if err != nil { - logging.Log.Error("Error getting device configuration values:", err) - return - } + case *model.ElectricalConnectionParameterDescriptionListDataType: + if e.evElectricalConnection == nil { + break + } + if _, err := e.evElectricalConnection.RequestPermittedValueSets(); err != nil { + logging.Log.Error("Error getting electrical permitted values:", err) + } - // TODO: provide the device configuration data - logging.Log.Debugf("Device Configuration Values: %#v\n", data) - */ - - /* - case *model.DeviceDiagnosisStateDataType: - var feature *features.DeviceDiagnosis - if entityType == model.EntityTypeTypeEVSE { - feature = e.evseDeviceDiagnosis - } else { - feature = e.evDeviceDiagnosis - } - _, err := feature.GetState() - if err != nil { - logging.Log.Error("Error getting device diagnosis state:", err) - } - */ - - /* - case *model.ElectricalConnectionDescriptionListDataType: - data, err := e.evElectricalConnection.GetDescription() - if err != nil { - logging.Log.Error("Error getting electrical description:", err) - return - } + case *model.LoadControlLimitDescriptionListDataType: + if e.evLoadControl == nil { + break + } + if _, err := e.evLoadControl.RequestLimitValues(); err != nil { + logging.Log.Error("Error getting loadcontrol limit values:", err) + } - // TODO: provide the electrical description data - logging.Log.Debugf("Electrical Description: %#v\n", data) - */ + case *model.MeasurementDescriptionListDataType: + if e.evMeasurement == nil { + break + } + if _, err := e.evMeasurement.RequestValues(); err != nil { + logging.Log.Error("Error getting measurement list values:", err) + } - case *model.ElectricalConnectionParameterDescriptionListDataType: - _, err := e.evElectricalConnection.RequestPermittedValueSet() - if err != nil { - logging.Log.Debug("Error getting electrical permitted values:", err) + case *model.TimeSeriesDescriptionListDataType: + if e.evTimeSeries == nil || payload.CmdClassifier == nil { + break } - /* - case *model.ElectricalConnectionPermittedValueSetListDataType: - data, err := e.evElectricalConnection.GetEVLimitValues() - if err != nil { - logging.Log.Error("Error getting electrical limit values:", err) - return + switch *payload.CmdClassifier { + case model.CmdClassifierTypeReply: + if err := e.evTimeSeries.RequestConstraints(); err == nil { + break + } + + // if constraints do not exist, directly request values + e.evRequestTimeSeriesValues() + + case model.CmdClassifierTypeNotify: + // check if we are required to update the plan + if !e.evCheckTimeSeriesDescriptionConstraintsUpdateRequired() { + break + } + + demand, err := e.EVEnergyDemand() + if err != nil { + logging.Log.Error("Error getting energy demand:", err) + break + } + + // request CEM for power limits + constraints := e.EVGetPowerConstraints() + if err != nil { + logging.Log.Error("Error getting timeseries constraints:", err) + } else { + if e.dataProvider == nil { + break } + e.dataProvider.EVRequestPowerLimits(demand, constraints) + } + } - // TODO: provide the electrical limit data - logging.Log.Debugf("Electrical Permitted Values: %#v\n", data) - */ + case *model.TimeSeriesConstraintsListDataType: + if e.evTimeSeries == nil || payload.CmdClassifier == nil { + break + } - case *model.LoadControlLimitDescriptionListDataType: - _, err := e.evLoadControl.RequestLimits() - if err != nil { - logging.Log.Debug("Error getting loadcontrol limit values:", err) + if *payload.CmdClassifier != model.CmdClassifierTypeReply { + break } - /* - case *model.LoadControlLimitListDataType: - data, err := e.evLoadControl.GetLimitValues() - if err != nil { - logging.Log.Error("Error getting loadcontrol limit values:", err) - return - } + e.evRequestTimeSeriesValues() - // TODO: provide the loadcontrol limit data - logging.Log.Debugf("Loadcontrol Limits: %#v\n", data) - */ + case *model.TimeSeriesListDataType: + if e.evTimeSeries == nil || payload.CmdClassifier == nil { + break + } - case *model.MeasurementDescriptionListDataType: - _, err := e.evMeasurement.Request() - if err != nil { - logging.Log.Debug("Error getting measurement list values:", err) + // check if we received a plan + e.evForwardChargePlanIfProvided() + + case *model.IncentiveDescriptionDataType: + if e.evIncentiveTable == nil || payload.CmdClassifier == nil { + break } - /* - case *model.MeasurementListDataType: - data, err := e.evMeasurement.GetValues() - if err != nil { - logging.Log.Error("Error getting measurement values:", err) - return - } + switch *payload.CmdClassifier { + case model.CmdClassifierTypeReply: + if err := e.evIncentiveTable.RequestConstraints(); err != nil { + break + } - // TODO: provide the measurement data - logging.Log.Debugf("Measurements: %#v\n", data) - */ + // if constraints do not exist, directly request values + e.evRequestIncentiveValues() - /* - case *model.IdentificationListDataType: - data, err := e.evIdentification.GetValues() - if err != nil { - logging.Log.Error("Error getting identification values:", err) - return - } + case model.CmdClassifierTypeNotify: + // check if we are required to update the plan + if e.dataProvider == nil || !e.evCheckIncentiveTableDescriptionUpdateRequired() { + break + } + + demand, err := e.EVEnergyDemand() + if err != nil { + logging.Log.Error("Error getting energy demand:", err) + break + } - // TODO: provide the device configuration data - logging.Log.Debugf("Identification Values: %#v\n", data) - */ + constraints := e.EVGetIncentiveConstraints() + + // request CEM for incentives + e.dataProvider.EVRequestIncentives(demand, constraints) + } + + case *model.IncentiveTableConstraintsDataType: + if *payload.CmdClassifier == model.CmdClassifierTypeReply { + e.evRequestIncentiveValues() + } } } } + + if e.dataProvider == nil { + return + } + + // check if the charge strategy changed + chargeStrategy := e.EVChargeStrategy() + if chargeStrategy == e.evCurrentChargeStrategy { + return + } + + // update the current value and inform the dataProvider + e.evCurrentChargeStrategy = chargeStrategy + e.dataProvider.EVProvidedChargeStrategy(chargeStrategy) +} + +// request time series values +func (e *EMobilityImpl) evRequestTimeSeriesValues() { + if e.evTimeSeries == nil { + return + } + + if _, err := e.evTimeSeries.RequestValues(); err != nil { + logging.Log.Error("Error getting time series list values:", err) + } +} + +// send the ev provided charge plan to the CEM +func (e *EMobilityImpl) evForwardChargePlanIfProvided() { + if data, err := e.evGetTimeSeriesPlanData(); err == nil { + e.dataProvider.EVProvidedChargePlan(data) + } +} + +func (e *EMobilityImpl) evGetTimeSeriesPlanData() ([]EVDurationSlotValue, error) { + if e.evTimeSeries == nil || e.dataProvider == nil { + return nil, ErrNotSupported + } + + timeSeries, err := e.evTimeSeries.GetValueForType(model.TimeSeriesTypeTypePlan) + if err != nil { + return nil, err + } + + if len(timeSeries.TimeSeriesSlot) == 0 { + return nil, ErrNotSupported + } + + var data []EVDurationSlotValue + + for _, slot := range timeSeries.TimeSeriesSlot { + duration, err := slot.Duration.GetTimeDuration() + if err != nil { + logging.Log.Error("ev charge plan contains invalid duration:", err) + return nil, err + } + + if slot.MaxValue == nil { + continue + } + + item := EVDurationSlotValue{ + Duration: duration, + Value: slot.MaxValue.GetValue(), + } + + data = append(data, item) + } + + if len(data) == 0 { + return nil, ErrNotSupported + } + + return data, nil +} + +// request incentive table values +func (e *EMobilityImpl) evRequestIncentiveValues() { + if e.evIncentiveTable == nil { + return + } + + if _, err := e.evIncentiveTable.RequestValues(); err != nil { + logging.Log.Error("Error getting time series list values:", err) + } + + e.evWriteIncentiveTableDescriptions() } // process required steps when an evse is connected @@ -209,8 +298,8 @@ func (e *EMobilityImpl) evseConnected(ski string, entity *spine.EntityRemoteImpl } e.evseDeviceDiagnosis = f2 - _, _ = e.evseDeviceClassification.RequestManufacturerDetailsForEntity() - _, _ = e.evseDeviceDiagnosis.RequestStateForEntity() + _, _ = e.evseDeviceClassification.RequestManufacturerDetails() + _, _ = e.evseDeviceDiagnosis.RequestState() } // an EV was disconnected @@ -238,11 +327,12 @@ func (e *EMobilityImpl) evDisconnected() { e.evMeasurement = nil e.evIdentification = nil e.evLoadControl = nil + e.evTimeSeries = nil + e.evIncentiveTable = nil logging.Log.Debug("ev disconnected") // TODO: add error handling - } // an EV was connected, trigger required communication @@ -252,8 +342,6 @@ func (e *EMobilityImpl) evConnected(entity *spine.EntityRemoteImpl) { logging.Log.Debug("ev connected") - // TODO: add error handling - // setup features e.evDeviceClassification, _ = features.NewDeviceClassification(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) e.evDeviceDiagnosis, _ = features.NewDeviceDiagnosis(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) @@ -262,96 +350,215 @@ func (e *EMobilityImpl) evConnected(entity *spine.EntityRemoteImpl) { e.evMeasurement, _ = features.NewMeasurement(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) e.evIdentification, _ = features.NewIdentification(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) e.evLoadControl, _ = features.NewLoadControl(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) + if e.configuration.CoordinatedChargingEnabled { + e.evTimeSeries, _ = features.NewTimeSeries(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) + e.evIncentiveTable, _ = features.NewIncentiveTable(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) + } + + // optional requests are only logged as debug // subscribe if err := e.evDeviceClassification.SubscribeForEntity(); err != nil { logging.Log.Debug(err) - return } if err := e.evDeviceConfiguration.SubscribeForEntity(); err != nil { logging.Log.Debug(err) - return } if err := e.evDeviceDiagnosis.SubscribeForEntity(); err != nil { logging.Log.Debug(err) - return } if err := e.evElectricalConnection.SubscribeForEntity(); err != nil { logging.Log.Debug(err) - return } if err := e.evMeasurement.SubscribeForEntity(); err != nil { logging.Log.Debug(err) - return } - if err := e.evIdentification.SubscribeForEntity(); err != nil { + if err := e.evLoadControl.SubscribeForEntity(); err != nil { logging.Log.Debug(err) - return } - if err := e.evLoadControl.SubscribeForEntity(); err != nil { + if err := e.evIdentification.SubscribeForEntity(); err != nil { logging.Log.Debug(err) - return } - // if err := util.SubscribeTimeSeriesForEntity(e.service, entity); err != nil { - // logging.Log.Error(err) - // return - // } - // if err := util.SubscribeIncentiveTableForEntity(e.service, entity); err != nil { - // logging.Log.Error(err) - // return - // } + + if e.configuration.CoordinatedChargingEnabled { + if err := e.evTimeSeries.SubscribeForEntity(); err != nil { + logging.Log.Debug(err) + } + // this is optional + if err := e.evIncentiveTable.SubscribeForEntity(); err != nil { + logging.Log.Debug(err) + } + } // bindings if err := e.evLoadControl.Bind(); err != nil { logging.Log.Debug(err) - return + } + + if e.configuration.CoordinatedChargingEnabled { + // this is optional + if err := e.evTimeSeries.Bind(); err != nil { + logging.Log.Debug(err) + } + + // this is optional + if err := e.evIncentiveTable.Bind(); err != nil { + logging.Log.Debug(err) + } } // get ev configuration data - if err := e.evDeviceConfiguration.Request(); err != nil { + if err := e.evDeviceConfiguration.RequestDescriptions(); err != nil { logging.Log.Debug(err) - return } // get manufacturer details - if _, err := e.evDeviceClassification.RequestManufacturerDetailsForEntity(); err != nil { + if _, err := e.evDeviceClassification.RequestManufacturerDetails(); err != nil { logging.Log.Debug(err) - return } // get device diagnosis state - if _, err := e.evDeviceDiagnosis.RequestStateForEntity(); err != nil { + if _, err := e.evDeviceDiagnosis.RequestState(); err != nil { logging.Log.Debug(err) - return } // get electrical connection parameter - if err := e.evElectricalConnection.RequestDescription(); err != nil { + if err := e.evElectricalConnection.RequestDescriptions(); err != nil { logging.Log.Debug(err) - return } - if err := e.evElectricalConnection.RequestParameterDescription(); err != nil { + if err := e.evElectricalConnection.RequestParameterDescriptions(); err != nil { logging.Log.Debug(err) - return } // get measurement parameters - if err := e.evMeasurement.RequestDescription(); err != nil { + if err := e.evMeasurement.RequestDescriptions(); err != nil { + logging.Log.Debug(err) + } + + // get loadlimit parameter + if err := e.evLoadControl.RequestLimitDescriptions(); err != nil { logging.Log.Debug(err) - return } // get identification - if _, err := e.evIdentification.Request(); err != nil { + if _, err := e.evIdentification.RequestValues(); err != nil { logging.Log.Debug(err) + } + + if e.configuration.CoordinatedChargingEnabled { + // get time series parameter + if err := e.evTimeSeries.RequestDescriptions(); err != nil { + logging.Log.Debug(err) + } + + // get incentive table parameter + if err := e.evIncentiveTable.RequestDescriptions(); err != nil { + logging.Log.Debug(err) + } + } +} + +// inform the EVSE about used currency and boundary units +// +// # SPINE UC CoordinatedEVCharging 2.4.3 +func (e *EMobilityImpl) evWriteIncentiveTableDescriptions() { + if e.evIncentiveTable == nil { return } - // get loadlimit parameter - if err := e.evLoadControl.RequestLimitDescription(); err != nil { - logging.Log.Debug(err) + descriptions, err := e.evIncentiveTable.GetDescriptionsForScope(model.ScopeTypeTypeSimpleIncentiveTable) + if err != nil { + logging.Log.Error(err) return } + // - tariff, min 1 + // each tariff has + // - tiers: min 1, max 3 + // each tier has: + // - boundaries: min 1, used for different power limits, e.g. 0-1kW x€, 1-3kW y€, ... + // - incentives: min 1, max 3 + // - price/costs (absolute or relative) + // - renewable energy percentage + // - CO2 emissions + // + // limit this to + // - 1 tariff + // - 1 tier + // - 1 boundary + // - 1 incentive (price) + // incentive type has to be the same for all sent power limits! + data := []model.IncentiveTableDescriptionType{ + { + TariffDescription: descriptions[0].TariffDescription, + Tier: []model.IncentiveTableDescriptionTierType{ + { + TierDescription: &model.TierDescriptionDataType{ + TierId: util.Ptr(model.TierIdType(1)), + TierType: util.Ptr(model.TierTypeTypeDynamicCost), + }, + BoundaryDescription: []model.TierBoundaryDescriptionDataType{ + { + BoundaryId: util.Ptr(model.TierBoundaryIdType(1)), + BoundaryType: util.Ptr(model.TierBoundaryTypeTypePowerBoundary), + BoundaryUnit: util.Ptr(model.UnitOfMeasurementTypeW), + }, + }, + IncentiveDescription: []model.IncentiveDescriptionDataType{ + { + IncentiveId: util.Ptr(model.IncentiveIdType(1)), + IncentiveType: util.Ptr(model.IncentiveTypeTypeAbsoluteCost), + Currency: util.Ptr(e.currency), + }, + }, + }, + }, + }, + } + + _, err = e.evIncentiveTable.WriteDescriptions(data) + if err != nil { + logging.Log.Error(err) + } +} + +// check timeSeries descriptions if constraints element has updateRequired set to true +// as this triggers the CEM to send power tables within 20s +func (e *EMobilityImpl) evCheckTimeSeriesDescriptionConstraintsUpdateRequired() bool { + if e.evTimeSeries == nil { + return false + } + + data, err := e.evTimeSeries.GetDescriptionForType(model.TimeSeriesTypeTypeConstraints) + if err != nil { + return false + } + + if data.UpdateRequired != nil { + return *data.UpdateRequired + } + + return false +} + +// check incentibeTable descriptions if the tariff description has updateRequired set to true +// as this triggers the CEM to send incentive tables within 20s +func (e *EMobilityImpl) evCheckIncentiveTableDescriptionUpdateRequired() bool { + if e.evIncentiveTable == nil { + return false + } + + data, err := e.evIncentiveTable.GetDescriptionsForScope(model.ScopeTypeTypeSimpleIncentiveTable) + if err != nil { + return false + } + + // only use the first description and therein the first tariff + item := data[0].TariffDescription + if item.UpdateRequired != nil { + return *item.UpdateRequired + } + + return false } diff --git a/emobility/events_evGetTimeSeriesPlanData_test.go b/emobility/events_evGetTimeSeriesPlanData_test.go new file mode 100644 index 0000000..75ab384 --- /dev/null +++ b/emobility/events_evGetTimeSeriesPlanData_test.go @@ -0,0 +1,107 @@ +package emobility + +import ( + "testing" + + "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/eebus-go/util" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" +) + +func Test_evGetTimeSeriesPlanData(t *testing.T) { + emobilty, eebusService := setupEmobility() + + data, err := emobilty.evGetTimeSeriesPlanData() + assert.NotNil(t, err) + assert.Nil(t, data) + + localDevice, remoteDevice, entites, _ := setupDevices(eebusService) + emobilty.evseEntity = entites[0] + emobilty.evEntity = entites[1] + + ctrl := gomock.NewController(t) + + dataProviderMock := NewMockEmobilityDataProvider(ctrl) + emobilty.dataProvider = dataProviderMock + + data, err = emobilty.evGetTimeSeriesPlanData() + assert.NotNil(t, err) + assert.Nil(t, data) + + emobilty.evTimeSeries = timeSeriesConfiguration(localDevice, emobilty.evEntity) + + data, err = emobilty.evGetTimeSeriesPlanData() + assert.NotNil(t, err) + assert.Nil(t, data) + + datagram := datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeServer, model.RoleTypeClient) + + cmd := []model.CmdType{{ + TimeSeriesDescriptionListData: &model.TimeSeriesDescriptionListDataType{ + TimeSeriesDescriptionData: []model.TimeSeriesDescriptionDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(1)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeConstraints), + TimeSeriesWriteable: util.Ptr(true), + UpdateRequired: util.Ptr(false), + Unit: util.Ptr(model.UnitOfMeasurementTypeW), + }, + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(2)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypePlan), + TimeSeriesWriteable: util.Ptr(false), + Unit: util.Ptr(model.UnitOfMeasurementTypeW), + }, + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(3)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeSingleDemand), + TimeSeriesWriteable: util.Ptr(false), + Unit: util.Ptr(model.UnitOfMeasurementTypeWh), + }, + }, + }}} + + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.evGetTimeSeriesPlanData() + assert.NotNil(t, err) + assert.Nil(t, data) + + cmd = []model.CmdType{{ + TimeSeriesListData: &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(2)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + Duration: util.Ptr(model.DurationType("PT5M36S")), + MaxValue: model.NewScaledNumberType(4201), + }, + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(1)), + Duration: util.Ptr(model.DurationType("P1D")), + MaxValue: model.NewScaledNumberType(0), + }, + }, + }, + }, + }}} + + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.evGetTimeSeriesPlanData() + assert.Nil(t, err) + assert.NotNil(t, data) + +} diff --git a/emobility/helper_test.go b/emobility/helper_test.go new file mode 100644 index 0000000..8e7c734 --- /dev/null +++ b/emobility/helper_test.go @@ -0,0 +1,381 @@ +package emobility + +import ( + "encoding/json" + "fmt" + "sync" + + "github.com/enbility/eebus-go/features" + "github.com/enbility/eebus-go/service" + "github.com/enbility/eebus-go/spine" + "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/eebus-go/util" +) + +type WriteMessageHandler struct { + sentMessages [][]byte + + mux sync.Mutex +} + +var _ spine.SpineDataConnection = (*WriteMessageHandler)(nil) + +func (t *WriteMessageHandler) WriteSpineMessage(message []byte) { + t.mux.Lock() + defer t.mux.Unlock() + + t.sentMessages = append(t.sentMessages, message) +} + +func (t *WriteMessageHandler) LastMessage() []byte { + t.mux.Lock() + defer t.mux.Unlock() + + if len(t.sentMessages) == 0 { + return nil + } + + return t.sentMessages[len(t.sentMessages)-1] +} + +func (t *WriteMessageHandler) MessageWithReference(msgCounterReference *model.MsgCounterType) []byte { + t.mux.Lock() + defer t.mux.Unlock() + + var datagram model.Datagram + + for _, msg := range t.sentMessages { + if err := json.Unmarshal(msg, &datagram); err != nil { + return nil + } + if datagram.Datagram.Header.MsgCounterReference == nil { + continue + } + if uint(*datagram.Datagram.Header.MsgCounterReference) != uint(*msgCounterReference) { + continue + } + if datagram.Datagram.Payload.Cmd[0].ResultData != nil { + continue + } + + return msg + } + + return nil +} + +func (t *WriteMessageHandler) ResultWithReference(msgCounterReference *model.MsgCounterType) []byte { + t.mux.Lock() + defer t.mux.Unlock() + + var datagram model.Datagram + + for _, msg := range t.sentMessages { + if err := json.Unmarshal(msg, &datagram); err != nil { + return nil + } + if datagram.Datagram.Header.MsgCounterReference == nil { + continue + } + if uint(*datagram.Datagram.Header.MsgCounterReference) != uint(*msgCounterReference) { + continue + } + if datagram.Datagram.Payload.Cmd[0].ResultData == nil { + continue + } + + return msg + } + + return nil +} + +const remoteSki string = "testremoteski" + +// we don't want to handle events in these tests for now, so we don't use NewEMobility(...) +func NewTestEMobility(service *service.EEBUSService, details *service.ServiceDetails) *EMobilityImpl { + ski := util.NormalizeSKI(details.SKI()) + + emobility := &EMobilityImpl{ + service: service, + entity: service.LocalEntity(), + ski: ski, + } + + service.PairRemoteService(details) + + return emobility +} + +func setupEmobility() (*EMobilityImpl, *service.EEBUSService) { + cert, _ := service.CreateCertificate("test", "test", "DE", "test") + configuration, _ := service.NewConfiguration("test", "test", "test", "test", model.DeviceTypeTypeEnergyManagementSystem, 9999, cert, 230.0) + eebusService := service.NewEEBUSService(configuration, nil) + _ = eebusService.Setup() + details := service.NewServiceDetails(remoteSki) + emobility := NewTestEMobility(eebusService, details) + return emobility, eebusService +} + +func setupDevices(eebusService *service.EEBUSService) (*spine.DeviceLocalImpl, *spine.DeviceRemoteImpl, []*spine.EntityRemoteImpl, *WriteMessageHandler) { + localDevice := eebusService.LocalDevice() + localEntity := localDevice.Entities()[1] + localDevice.AddEntity(localEntity) + + f := spine.NewFeatureLocalImpl(1, localEntity, model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeClient) + localEntity.AddFeature(f) + f = spine.NewFeatureLocalImpl(2, localEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeClient) + localEntity.AddFeature(f) + f = spine.NewFeatureLocalImpl(3, localEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeClient) + localEntity.AddFeature(f) + f = spine.NewFeatureLocalImpl(4, localEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeClient) + localEntity.AddFeature(f) + f = spine.NewFeatureLocalImpl(5, localEntity, model.FeatureTypeTypeIdentification, model.RoleTypeClient) + localEntity.AddFeature(f) + f = spine.NewFeatureLocalImpl(6, localEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeClient) + localEntity.AddFeature(f) + f = spine.NewFeatureLocalImpl(6, localEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeClient) + localEntity.AddFeature(f) + f = spine.NewFeatureLocalImpl(6, localEntity, model.FeatureTypeTypeIncentiveTable, model.RoleTypeClient) + localEntity.AddFeature(f) + + writeHandler := &WriteMessageHandler{} + remoteDevice := spine.NewDeviceRemoteImpl(localDevice, remoteSki, writeHandler) + + var clientRemoteFeatures = []struct { + featureType model.FeatureTypeType + supportedFcts []model.FunctionType + }{ + { + model.FeatureTypeTypeDeviceDiagnosis, + []model.FunctionType{}, + }, + { + model.FeatureTypeTypeDeviceConfiguration, + []model.FunctionType{ + model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, + model.FunctionTypeDeviceConfigurationKeyValueListData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionDescriptionListData, + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + model.FunctionTypeElectricalConnectionPermittedValueSetListData, + }, + }, + { + model.FeatureTypeTypeMeasurement, + []model.FunctionType{ + model.FunctionTypeMeasurementDescriptionListData, + model.FunctionTypeMeasurementListData, + }, + }, + { + model.FeatureTypeTypeLoadControl, + []model.FunctionType{ + model.FunctionTypeLoadControlLimitDescriptionListData, + model.FunctionTypeLoadControlLimitListData, + }, + }, + { + model.FeatureTypeTypeIdentification, + []model.FunctionType{ + model.FunctionTypeIdentificationListData, + }, + }, + {model.FeatureTypeTypeTimeSeries, + []model.FunctionType{ + model.FunctionTypeTimeSeriesDescriptionListData, + model.FunctionTypeTimeSeriesListData, + model.FunctionTypeTimeSeriesConstraintsListData, + }, + }, + {model.FeatureTypeTypeIncentiveTable, + []model.FunctionType{ + model.FunctionTypeIncentiveTableConstraintsData, + }, + }, + } + + remoteDeviceName := "remote" + + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range clientRemoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: util.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + Feature: util.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: util.Ptr(feature.featureType), + Role: util.Ptr(model.RoleTypeServer), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeEVSE), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeEV), + }, + }, + }, + FeatureInformation: featureInformations, + } + localDevice.AddRemoteDeviceForSki(remoteSki, remoteDevice) + + entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + + return localDevice, remoteDevice, entities, writeHandler +} + +func datagramForEntityAndFeatures(notify bool, localDevice *spine.DeviceLocalImpl, remoteEntity *spine.EntityRemoteImpl, featureType model.FeatureTypeType, remoteRole, localRole model.RoleType) model.DatagramType { + var addressSource, addressDestination *model.FeatureAddressType + if remoteEntity == nil { + // NodeManagement + addressSource = &model.FeatureAddressType{ + Entity: []model.AddressEntityType{0}, + Feature: util.Ptr(model.AddressFeatureType(0)), + } + addressDestination = &model.FeatureAddressType{ + Device: localDevice.Address(), + Entity: []model.AddressEntityType{0}, + Feature: util.Ptr(model.AddressFeatureType(0)), + } + } else { + rFeature := featureOfTypeAndRole(remoteEntity, featureType, remoteRole) + addressSource = rFeature.Address() + + lFeature := localDevice.FeatureByTypeAndRole(featureType, localRole) + addressDestination = lFeature.Address() + } + datagram := model.DatagramType{ + Header: model.HeaderType{ + AddressSource: addressSource, + AddressDestination: addressDestination, + MsgCounter: util.Ptr(model.MsgCounterType(1)), + MsgCounterReference: util.Ptr(model.MsgCounterType(1)), + CmdClassifier: util.Ptr(model.CmdClassifierTypeReply), + }, + Payload: model.PayloadType{ + Cmd: []model.CmdType{}, + }, + } + if notify { + datagram.Header.CmdClassifier = util.Ptr(model.CmdClassifierTypeNotify) + } + + return datagram +} + +func featureOfTypeAndRole(entity *spine.EntityRemoteImpl, featureType model.FeatureTypeType, role model.RoleType) *spine.FeatureRemoteImpl { + for _, f := range entity.Features() { + if f.Type() == featureType && f.Role() == role { + return f + } + } + return nil +} + +func deviceDiagnosis(localDevice *spine.DeviceLocalImpl, entity *spine.EntityRemoteImpl) *features.DeviceDiagnosis { + feature, err := features.NewDeviceDiagnosis(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) + if err != nil { + fmt.Println(err) + } + return feature +} + +func electricalConnection(localDevice *spine.DeviceLocalImpl, entity *spine.EntityRemoteImpl) *features.ElectricalConnection { + feature, err := features.NewElectricalConnection(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) + if err != nil { + fmt.Println(err) + } + return feature +} + +func measurement(localDevice *spine.DeviceLocalImpl, entity *spine.EntityRemoteImpl) *features.Measurement { + feature, err := features.NewMeasurement(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) + if err != nil { + fmt.Println(err) + } + return feature +} + +func deviceConfiguration(localDevice *spine.DeviceLocalImpl, entity *spine.EntityRemoteImpl) *features.DeviceConfiguration { + feature, err := features.NewDeviceConfiguration(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) + if err != nil { + fmt.Println(err) + } + return feature +} + +func identificationConfiguration(localDevice *spine.DeviceLocalImpl, entity *spine.EntityRemoteImpl) *features.Identification { + feature, err := features.NewIdentification(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) + if err != nil { + fmt.Println(err) + } + return feature +} + +func loadcontrol(localDevice *spine.DeviceLocalImpl, entity *spine.EntityRemoteImpl) *features.LoadControl { + feature, err := features.NewLoadControl(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) + if err != nil { + fmt.Println(err) + } + return feature +} + +func timeSeriesConfiguration(localDevice *spine.DeviceLocalImpl, entity *spine.EntityRemoteImpl) *features.TimeSeries { + feature, err := features.NewTimeSeries(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) + if err != nil { + fmt.Println(err) + } + return feature +} + +func incentiveTableConfiguration(localDevice *spine.DeviceLocalImpl, entity *spine.EntityRemoteImpl) *features.IncentiveTable { + feature, err := features.NewIncentiveTable(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) + if err != nil { + fmt.Println(err) + } + return feature +} diff --git a/emobility/mock_emobility.go b/emobility/mock_emobility.go new file mode 100644 index 0000000..964035b --- /dev/null +++ b/emobility/mock_emobility.go @@ -0,0 +1,401 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: emobility.go + +// Package emobility is a generated GoMock package. +package emobility + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockEmobilityDataProvider is a mock of EmobilityDataProvider interface. +type MockEmobilityDataProvider struct { + ctrl *gomock.Controller + recorder *MockEmobilityDataProviderMockRecorder +} + +// MockEmobilityDataProviderMockRecorder is the mock recorder for MockEmobilityDataProvider. +type MockEmobilityDataProviderMockRecorder struct { + mock *MockEmobilityDataProvider +} + +// NewMockEmobilityDataProvider creates a new mock instance. +func NewMockEmobilityDataProvider(ctrl *gomock.Controller) *MockEmobilityDataProvider { + mock := &MockEmobilityDataProvider{ctrl: ctrl} + mock.recorder = &MockEmobilityDataProviderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockEmobilityDataProvider) EXPECT() *MockEmobilityDataProviderMockRecorder { + return m.recorder +} + +// EVProvidedChargePlan mocks base method. +func (m *MockEmobilityDataProvider) EVProvidedChargePlan(data []EVDurationSlotValue) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "EVProvidedChargePlan", data) +} + +// EVProvidedChargePlan indicates an expected call of EVProvidedChargePlan. +func (mr *MockEmobilityDataProviderMockRecorder) EVProvidedChargePlan(data interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVProvidedChargePlan", reflect.TypeOf((*MockEmobilityDataProvider)(nil).EVProvidedChargePlan), data) +} + +// EVProvidedChargeStrategy mocks base method. +func (m *MockEmobilityDataProvider) EVProvidedChargeStrategy(strategy EVChargeStrategyType) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "EVProvidedChargeStrategy", strategy) +} + +// EVProvidedChargeStrategy indicates an expected call of EVProvidedChargeStrategy. +func (mr *MockEmobilityDataProviderMockRecorder) EVProvidedChargeStrategy(strategy interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVProvidedChargeStrategy", reflect.TypeOf((*MockEmobilityDataProvider)(nil).EVProvidedChargeStrategy), strategy) +} + +// EVRequestIncentives mocks base method. +func (m *MockEmobilityDataProvider) EVRequestIncentives(demand EVDemand, constraints EVIncentiveSlotConstraints) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "EVRequestIncentives", demand, constraints) +} + +// EVRequestIncentives indicates an expected call of EVRequestIncentives. +func (mr *MockEmobilityDataProviderMockRecorder) EVRequestIncentives(demand, constraints interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVRequestIncentives", reflect.TypeOf((*MockEmobilityDataProvider)(nil).EVRequestIncentives), demand, constraints) +} + +// EVRequestPowerLimits mocks base method. +func (m *MockEmobilityDataProvider) EVRequestPowerLimits(demand EVDemand, constraints EVTimeSlotConstraints) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "EVRequestPowerLimits", demand, constraints) +} + +// EVRequestPowerLimits indicates an expected call of EVRequestPowerLimits. +func (mr *MockEmobilityDataProviderMockRecorder) EVRequestPowerLimits(demand, constraints interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVRequestPowerLimits", reflect.TypeOf((*MockEmobilityDataProvider)(nil).EVRequestPowerLimits), demand, constraints) +} + +// MockEmobilityI is a mock of EmobilityI interface. +type MockEmobilityI struct { + ctrl *gomock.Controller + recorder *MockEmobilityIMockRecorder +} + +// MockEmobilityIMockRecorder is the mock recorder for MockEmobilityI. +type MockEmobilityIMockRecorder struct { + mock *MockEmobilityI +} + +// NewMockEmobilityI creates a new mock instance. +func NewMockEmobilityI(ctrl *gomock.Controller) *MockEmobilityI { + mock := &MockEmobilityI{ctrl: ctrl} + mock.recorder = &MockEmobilityIMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockEmobilityI) EXPECT() *MockEmobilityIMockRecorder { + return m.recorder +} + +// EVChargeStrategy mocks base method. +func (m *MockEmobilityI) EVChargeStrategy() EVChargeStrategyType { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EVChargeStrategy") + ret0, _ := ret[0].(EVChargeStrategyType) + return ret0 +} + +// EVChargeStrategy indicates an expected call of EVChargeStrategy. +func (mr *MockEmobilityIMockRecorder) EVChargeStrategy() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVChargeStrategy", reflect.TypeOf((*MockEmobilityI)(nil).EVChargeStrategy)) +} + +// EVChargedEnergy mocks base method. +func (m *MockEmobilityI) EVChargedEnergy() (float64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EVChargedEnergy") + ret0, _ := ret[0].(float64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EVChargedEnergy indicates an expected call of EVChargedEnergy. +func (mr *MockEmobilityIMockRecorder) EVChargedEnergy() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVChargedEnergy", reflect.TypeOf((*MockEmobilityI)(nil).EVChargedEnergy)) +} + +// EVCommunicationStandard mocks base method. +func (m *MockEmobilityI) EVCommunicationStandard() (EVCommunicationStandardType, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EVCommunicationStandard") + ret0, _ := ret[0].(EVCommunicationStandardType) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EVCommunicationStandard indicates an expected call of EVCommunicationStandard. +func (mr *MockEmobilityIMockRecorder) EVCommunicationStandard() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVCommunicationStandard", reflect.TypeOf((*MockEmobilityI)(nil).EVCommunicationStandard)) +} + +// EVConnectedPhases mocks base method. +func (m *MockEmobilityI) EVConnectedPhases() (uint, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EVConnectedPhases") + ret0, _ := ret[0].(uint) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EVConnectedPhases indicates an expected call of EVConnectedPhases. +func (mr *MockEmobilityIMockRecorder) EVConnectedPhases() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVConnectedPhases", reflect.TypeOf((*MockEmobilityI)(nil).EVConnectedPhases)) +} + +// EVCoordinatedChargingSupported mocks base method. +func (m *MockEmobilityI) EVCoordinatedChargingSupported() (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EVCoordinatedChargingSupported") + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EVCoordinatedChargingSupported indicates an expected call of EVCoordinatedChargingSupported. +func (mr *MockEmobilityIMockRecorder) EVCoordinatedChargingSupported() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVCoordinatedChargingSupported", reflect.TypeOf((*MockEmobilityI)(nil).EVCoordinatedChargingSupported)) +} + +// EVCurrentChargeState mocks base method. +func (m *MockEmobilityI) EVCurrentChargeState() (EVChargeStateType, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EVCurrentChargeState") + ret0, _ := ret[0].(EVChargeStateType) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EVCurrentChargeState indicates an expected call of EVCurrentChargeState. +func (mr *MockEmobilityIMockRecorder) EVCurrentChargeState() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVCurrentChargeState", reflect.TypeOf((*MockEmobilityI)(nil).EVCurrentChargeState)) +} + +// EVCurrentLimits mocks base method. +func (m *MockEmobilityI) EVCurrentLimits() ([]float64, []float64, []float64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EVCurrentLimits") + ret0, _ := ret[0].([]float64) + ret1, _ := ret[1].([]float64) + ret2, _ := ret[2].([]float64) + ret3, _ := ret[3].(error) + return ret0, ret1, ret2, ret3 +} + +// EVCurrentLimits indicates an expected call of EVCurrentLimits. +func (mr *MockEmobilityIMockRecorder) EVCurrentLimits() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVCurrentLimits", reflect.TypeOf((*MockEmobilityI)(nil).EVCurrentLimits)) +} + +// EVCurrentsPerPhase mocks base method. +func (m *MockEmobilityI) EVCurrentsPerPhase() ([]float64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EVCurrentsPerPhase") + ret0, _ := ret[0].([]float64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EVCurrentsPerPhase indicates an expected call of EVCurrentsPerPhase. +func (mr *MockEmobilityIMockRecorder) EVCurrentsPerPhase() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVCurrentsPerPhase", reflect.TypeOf((*MockEmobilityI)(nil).EVCurrentsPerPhase)) +} + +// EVEnergyDemand mocks base method. +func (m *MockEmobilityI) EVEnergyDemand() (EVDemand, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EVEnergyDemand") + ret0, _ := ret[0].(EVDemand) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EVEnergyDemand indicates an expected call of EVEnergyDemand. +func (mr *MockEmobilityIMockRecorder) EVEnergyDemand() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVEnergyDemand", reflect.TypeOf((*MockEmobilityI)(nil).EVEnergyDemand)) +} + +// EVGetIncentiveConstraints mocks base method. +func (m *MockEmobilityI) EVGetIncentiveConstraints() EVIncentiveSlotConstraints { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EVGetIncentiveConstraints") + ret0, _ := ret[0].(EVIncentiveSlotConstraints) + return ret0 +} + +// EVGetIncentiveConstraints indicates an expected call of EVGetIncentiveConstraints. +func (mr *MockEmobilityIMockRecorder) EVGetIncentiveConstraints() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVGetIncentiveConstraints", reflect.TypeOf((*MockEmobilityI)(nil).EVGetIncentiveConstraints)) +} + +// EVGetPowerConstraints mocks base method. +func (m *MockEmobilityI) EVGetPowerConstraints() EVTimeSlotConstraints { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EVGetPowerConstraints") + ret0, _ := ret[0].(EVTimeSlotConstraints) + return ret0 +} + +// EVGetPowerConstraints indicates an expected call of EVGetPowerConstraints. +func (mr *MockEmobilityIMockRecorder) EVGetPowerConstraints() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVGetPowerConstraints", reflect.TypeOf((*MockEmobilityI)(nil).EVGetPowerConstraints)) +} + +// EVIdentification mocks base method. +func (m *MockEmobilityI) EVIdentification() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EVIdentification") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EVIdentification indicates an expected call of EVIdentification. +func (mr *MockEmobilityIMockRecorder) EVIdentification() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVIdentification", reflect.TypeOf((*MockEmobilityI)(nil).EVIdentification)) +} + +// EVLoadControlObligationLimits mocks base method. +func (m *MockEmobilityI) EVLoadControlObligationLimits() ([]float64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EVLoadControlObligationLimits") + ret0, _ := ret[0].([]float64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EVLoadControlObligationLimits indicates an expected call of EVLoadControlObligationLimits. +func (mr *MockEmobilityIMockRecorder) EVLoadControlObligationLimits() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVLoadControlObligationLimits", reflect.TypeOf((*MockEmobilityI)(nil).EVLoadControlObligationLimits)) +} + +// EVOptimizationOfSelfConsumptionSupported mocks base method. +func (m *MockEmobilityI) EVOptimizationOfSelfConsumptionSupported() (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EVOptimizationOfSelfConsumptionSupported") + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EVOptimizationOfSelfConsumptionSupported indicates an expected call of EVOptimizationOfSelfConsumptionSupported. +func (mr *MockEmobilityIMockRecorder) EVOptimizationOfSelfConsumptionSupported() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVOptimizationOfSelfConsumptionSupported", reflect.TypeOf((*MockEmobilityI)(nil).EVOptimizationOfSelfConsumptionSupported)) +} + +// EVPowerPerPhase mocks base method. +func (m *MockEmobilityI) EVPowerPerPhase() ([]float64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EVPowerPerPhase") + ret0, _ := ret[0].([]float64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EVPowerPerPhase indicates an expected call of EVPowerPerPhase. +func (mr *MockEmobilityIMockRecorder) EVPowerPerPhase() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVPowerPerPhase", reflect.TypeOf((*MockEmobilityI)(nil).EVPowerPerPhase)) +} + +// EVSoC mocks base method. +func (m *MockEmobilityI) EVSoC() (float64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EVSoC") + ret0, _ := ret[0].(float64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EVSoC indicates an expected call of EVSoC. +func (mr *MockEmobilityIMockRecorder) EVSoC() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVSoC", reflect.TypeOf((*MockEmobilityI)(nil).EVSoC)) +} + +// EVSoCSupported mocks base method. +func (m *MockEmobilityI) EVSoCSupported() (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EVSoCSupported") + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EVSoCSupported indicates an expected call of EVSoCSupported. +func (mr *MockEmobilityIMockRecorder) EVSoCSupported() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVSoCSupported", reflect.TypeOf((*MockEmobilityI)(nil).EVSoCSupported)) +} + +// EVWriteIncentives mocks base method. +func (m *MockEmobilityI) EVWriteIncentives(data []EVDurationSlotValue) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EVWriteIncentives", data) + ret0, _ := ret[0].(error) + return ret0 +} + +// EVWriteIncentives indicates an expected call of EVWriteIncentives. +func (mr *MockEmobilityIMockRecorder) EVWriteIncentives(data interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVWriteIncentives", reflect.TypeOf((*MockEmobilityI)(nil).EVWriteIncentives), data) +} + +// EVWriteLoadControlLimits mocks base method. +func (m *MockEmobilityI) EVWriteLoadControlLimits(obligations, recommendations []float64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EVWriteLoadControlLimits", obligations, recommendations) + ret0, _ := ret[0].(error) + return ret0 +} + +// EVWriteLoadControlLimits indicates an expected call of EVWriteLoadControlLimits. +func (mr *MockEmobilityIMockRecorder) EVWriteLoadControlLimits(obligations, recommendations interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVWriteLoadControlLimits", reflect.TypeOf((*MockEmobilityI)(nil).EVWriteLoadControlLimits), obligations, recommendations) +} + +// EVWritePowerLimits mocks base method. +func (m *MockEmobilityI) EVWritePowerLimits(data []EVDurationSlotValue) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EVWritePowerLimits", data) + ret0, _ := ret[0].(error) + return ret0 +} + +// EVWritePowerLimits indicates an expected call of EVWritePowerLimits. +func (mr *MockEmobilityIMockRecorder) EVWritePowerLimits(data interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVWritePowerLimits", reflect.TypeOf((*MockEmobilityI)(nil).EVWritePowerLimits), data) +} diff --git a/emobility/public.go b/emobility/public.go index 9cd300c..ec350b6 100644 --- a/emobility/public.go +++ b/emobility/public.go @@ -1,13 +1,15 @@ package emobility import ( + "errors" + "time" + "github.com/enbility/cemd/util" "github.com/enbility/eebus-go/features" "github.com/enbility/eebus-go/spine/model" + eebusUtil "github.com/enbility/eebus-go/util" ) -var phaseMapping = []string{"a", "b", "c"} - // return the current charge sate of the EV func (e *EMobilityImpl) EVCurrentChargeState() (EVChargeStateType, error) { if e.evEntity == nil { @@ -23,7 +25,12 @@ func (e *EMobilityImpl) EVCurrentChargeState() (EVChargeStateType, error) { return EVChargeStateTypeUnknown, err } - switch diagnosisState.OperatingState { + operatingState := diagnosisState.OperatingState + if operatingState == nil { + return EVChargeStateTypeUnknown, features.ErrDataNotAvailable + } + + switch *operatingState { case model.DeviceDiagnosisOperatingStateTypeNormalOperation: return EVChargeStateTypeActive, nil case model.DeviceDiagnosisOperatingStateTypeStandby: @@ -47,7 +54,23 @@ func (e *EMobilityImpl) EVConnectedPhases() (uint, error) { return 0, features.ErrDataNotAvailable } - return e.evElectricalConnection.GetConnectedPhases() + data, err := e.evElectricalConnection.GetDescriptions() + if err != nil { + return 0, features.ErrDataNotAvailable + } + + for _, item := range data { + if item.ElectricalConnectionId == nil { + continue + } + + if item.AcConnectedPhases != nil { + return *item.AcConnectedPhases, nil + } + } + + // default to 3 if the value is not available + return 3, nil } // return the charged energy measurement in Wh of the connected EV @@ -57,14 +80,28 @@ func (e *EMobilityImpl) EVConnectedPhases() (uint, error) { // - and others func (e *EMobilityImpl) EVChargedEnergy() (float64, error) { if e.evEntity == nil { - return 0.0, ErrEVDisconnected + return 0, ErrEVDisconnected } if e.evMeasurement == nil { - return 0.0, features.ErrDataNotAvailable + return 0, features.ErrDataNotAvailable + } + + measurement := model.MeasurementTypeTypeEnergy + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeCharge + data, err := e.evMeasurement.GetValuesForTypeCommodityScope(measurement, commodity, scope) + if err != nil { + return 0, err + } + + // we assume there is only one result + value := data[0].Value + if value == nil { + return 0, features.ErrDataNotAvailable } - return e.evMeasurement.GetValueForScope(model.ScopeTypeTypeCharge, e.evElectricalConnection) + return value.GetValue(), err } // return the last power measurement for each phase of the connected EV @@ -81,36 +118,45 @@ func (e *EMobilityImpl) EVPowerPerPhase() ([]float64, error) { return nil, features.ErrDataNotAvailable } - data, err := e.evMeasurement.GetValuesPerPhaseForScope(model.ScopeTypeTypeACPower, e.evElectricalConnection) - if err != nil { - return nil, err - } + var data []model.MeasurementDataType - // If power is not provided, fall back to power calculations via currents - if len(data) == 0 { - currents, err := e.evMeasurement.GetValuesPerPhaseForScope(model.ScopeTypeTypeACCurrent, e.evElectricalConnection) + powerAvailable := true + measurement := model.MeasurementTypeTypePower + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeACPower + data, err := e.evMeasurement.GetValuesForTypeCommodityScope(measurement, commodity, scope) + if err != nil || len(data) == 0 { + powerAvailable = false + + // If power is not provided, fall back to power calculations via currents + measurement = model.MeasurementTypeTypeCurrent + scope = model.ScopeTypeTypeACCurrent + data, err = e.evMeasurement.GetValuesForTypeCommodityScope(measurement, commodity, scope) if err != nil { return nil, err } - - // calculate the power - for _, phase := range phaseMapping { - value := 0.0 - if theValue, exists := currents[phase]; exists { - value = theValue - } - data[phase] = value * e.service.Configuration.Voltage() - } } var result []float64 - for _, phase := range phaseMapping { - value := 0.0 - if theValue, exists := data[phase]; exists { - value = theValue + for _, phase := range util.PhaseNameMapping { + for _, item := range data { + if item.Value == nil { + continue + } + + elParam, err := e.evElectricalConnection.GetParameterDescriptionForMeasurementId(*item.MeasurementId) + if err != nil || elParam.AcMeasuredPhases == nil || *elParam.AcMeasuredPhases != phase { + continue + } + + phaseValue := item.Value.GetValue() + if !powerAvailable { + phaseValue *= e.service.Configuration.Voltage() + } + + result = append(result, phaseValue) } - result = append(result, value) } return result, nil @@ -130,19 +176,30 @@ func (e *EMobilityImpl) EVCurrentsPerPhase() ([]float64, error) { return nil, features.ErrDataNotAvailable } - data, err := e.evMeasurement.GetValuesPerPhaseForScope(model.ScopeTypeTypeACCurrent, e.evElectricalConnection) + measurement := model.MeasurementTypeTypeCurrent + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeACCurrent + data, err := e.evMeasurement.GetValuesForTypeCommodityScope(measurement, commodity, scope) if err != nil { return nil, err } var result []float64 - for _, phase := range phaseMapping { - value := 0.0 - if theValue, exists := data[phase]; exists { - value = theValue + for _, phase := range util.PhaseNameMapping { + for _, item := range data { + if item.Value == nil { + continue + } + + elParam, err := e.evElectricalConnection.GetParameterDescriptionForMeasurementId(*item.MeasurementId) + if err != nil || elParam.AcMeasuredPhases == nil || *elParam.AcMeasuredPhases != phase { + continue + } + + phaseValue := item.Value.GetValue() + result = append(result, phaseValue) } - result = append(result, value) } return result, nil @@ -162,44 +219,98 @@ func (e *EMobilityImpl) EVCurrentLimits() ([]float64, []float64, []float64, erro return nil, nil, nil, features.ErrDataNotAvailable } - dataMin, dataMax, dataDefault, err := e.evElectricalConnection.GetCurrentsLimits() + var resultMin, resultMax, resultDefault []float64 + + for _, phaseName := range util.PhaseNameMapping { + // electricalParameterDescription contains the measured phase for each measurementId + elParamDesc, err := e.evElectricalConnection.GetParameterDescriptionForMeasuredPhase(phaseName) + if err != nil || elParamDesc.ParameterId == nil { + continue + } + + dataMin, dataMax, dataDefault, err := e.evElectricalConnection.GetLimitsForParameterId(*elParamDesc.ParameterId) + if err != nil { + continue + } + + // Min current data should be derived from min power data + // but as this value is only properly provided via VAS the + // currrent min values can not be trusted. + // Min current for 3-phase should be at least 2.2A (ISO) + if dataMin < 2.2 { + dataMin = 2.2 + } + + resultMin = append(resultMin, dataMin) + resultMax = append(resultMax, dataMax) + resultDefault = append(resultDefault, dataDefault) + } + + if len(resultMin) == 0 { + return nil, nil, nil, features.ErrDataNotAvailable + } + return resultMin, resultMax, resultDefault, nil +} + +// return the current loadcontrol obligation limits +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (e *EMobilityImpl) EVLoadControlObligationLimits() ([]float64, error) { + if e.evEntity == nil { + return nil, ErrEVDisconnected + } + + if e.evElectricalConnection == nil || e.evLoadControl == nil { + return nil, features.ErrDataNotAvailable + } + + // find out the appropriate limitId for each phase value + // limitDescription contains the measurementId for each limitId + limitDescriptions, err := e.evLoadControl.GetLimitDescriptionsForCategory(model.LoadControlCategoryTypeObligation) if err != nil { - return nil, nil, nil, err + return nil, features.ErrDataNotAvailable } - var resultMin, resultMax, resultDefault []float64 + var result []float64 + + for i := 0; i < 3; i++ { + phaseName := util.PhaseNameMapping[i] - for _, phase := range phaseMapping { - value := 0.0 - if theValue, exists := dataMin[phase]; exists { - value = theValue + // electricalParameterDescription contains the measured phase for each measurementId + elParamDesc, err := e.evElectricalConnection.GetParameterDescriptionForMeasuredPhase(phaseName) + if err != nil || elParamDesc.MeasurementId == nil { + // there is no data for this phase, the phase may not exit + result = append(result, 0) + continue } - resultMin = append(resultMin, value) - value = 0.0 - if theValue, exists := dataMax[phase]; exists { - value = theValue + var limitDesc *model.LoadControlLimitDescriptionDataType + for _, desc := range limitDescriptions { + if desc.MeasurementId != nil && *desc.MeasurementId == *elParamDesc.MeasurementId { + limitDesc = &desc + break + } } - resultMax = append(resultMax, value) - value = 0.0 - if theValue, exists := dataDefault[phase]; exists { - value = theValue + if limitDesc == nil || limitDesc.LimitId == nil { + return nil, features.ErrDataNotAvailable + } + + limitIdData, err := e.evLoadControl.GetLimitValueForLimitId(*limitDesc.LimitId) + if err != nil { + return nil, features.ErrDataNotAvailable } - resultDefault = append(resultDefault, value) - } - // Min current data should be derived from min power data - // but as this value is only properly provided via VAS the - // currrent min values can not be trusted. - // Min current for 3-phase should be at least 2.2A (ISO) - for index, item := range resultMin { - if item < 2.2 { - resultMin[index] = 2.2 + if limitIdData.Value == nil { + return nil, features.ErrDataNotAvailable } + + result = append(result, limitIdData.Value.GetValue()) } - return resultMin, resultMax, resultDefault, nil + return result, nil } // send new LoadControlLimits to the remote EV @@ -222,13 +333,12 @@ func (e *EMobilityImpl) EVCurrentLimits() ([]float64, []float64, []float64, erro // the EVSE needs to be able map the recommendations into oligation limits which then // works for all EVs communication either via IEC61851 or ISO15118. // -// note: -// For obligations to work for optimizing solar excess power, the EV needs to -// have an energy demand. Recommendations work even if the EV does not have an active -// energy demand, given it communicated with the EVSE via ISO15118 and supports the usecase. -// In ISO15118-2 the usecase is only supported via VAS extensions which are vendor specific -// and needs to have specific EVSE support for the specific EV brand. -// In ISO15118-20 this is a standard feature which does not need special support on the EVSE. +// notes: +// - For obligations to work for optimizing solar excess power, the EV needs to have an energy demand. +// - Recommendations work even if the EV does not have an active energy demand, given it communicated with the EVSE via ISO15118 and supports the usecase. +// - In ISO15118-2 the usecase is only supported via VAS extensions which are vendor specific and needs to have specific EVSE support for the specific EV brand. +// - In ISO15118-20 this is a standard feature which does not need special support on the EVSE. +// - Min power data is only provided via IEC61851 or using VAS in ISO15118-2. func (e *EMobilityImpl) EVWriteLoadControlLimits(obligations, recommendations []float64) error { if e.evEntity == nil { return ErrEVDisconnected @@ -238,26 +348,6 @@ func (e *EMobilityImpl) EVWriteLoadControlLimits(obligations, recommendations [] return features.ErrDataNotAvailable } - electricalDesc, _, err := e.evElectricalConnection.GetParamDescriptionListData() - if err != nil { - return features.ErrMetadataNotAvailable - } - - elLimits, err := e.evElectricalConnection.GetEVLimitValues() - if err != nil { - return features.ErrMetadataNotAvailable - } - - limitDesc, err := e.evLoadControl.GetLimitDescription() - if err != nil { - return err - } - - currentLimits, err := e.evLoadControl.GetLimitValues() - if err != nil { - return err - } - var limitData []model.LoadControlLimitDataType for scopeTypes := 0; scopeTypes < 2; scopeTypes++ { @@ -268,79 +358,58 @@ func (e *EMobilityImpl) EVWriteLoadControlLimits(obligations, recommendations [] currentsPerPhase = recommendations } - for index, limit := range currentsPerPhase { - phase := phaseMapping[index] + for index, phaseLimit := range currentsPerPhase { + phaseName := util.PhaseNameMapping[index] - var limitId *model.LoadControlLimitIdType - var elConnectionid *model.ElectricalConnectionIdType - - for _, lDesc := range limitDesc { - if lDesc.LimitCategory == nil || lDesc.MeasurementId == nil { - continue - } + // find out the appropriate limitId for each phase value + // limitDescription contains the measurementId for each limitId + limitDescriptions, err := e.evLoadControl.GetLimitDescriptionsForCategory(category) + if err != nil { + continue + } - if *lDesc.LimitCategory != category { - continue - } + // electricalParameterDescription contains the measured phase for each measurementId + elParamDesc, err := e.evElectricalConnection.GetParameterDescriptionForMeasuredPhase(phaseName) + if err != nil || elParamDesc.MeasurementId == nil { + continue + } - elDesc, exists := electricalDesc[*lDesc.MeasurementId] - if !exists { - continue + var limitDesc *model.LoadControlLimitDescriptionDataType + for _, desc := range limitDescriptions { + if desc.MeasurementId != nil && *desc.MeasurementId == *elParamDesc.MeasurementId { + limitDesc = &desc + break } - if elDesc.ElectricalConnectionId == nil || elDesc.AcMeasuredPhases == nil || string(*elDesc.AcMeasuredPhases) != phase { - continue - } - - limitId = lDesc.LimitId - elConnectionid = elDesc.ElectricalConnectionId - break } - if limitId == nil || elConnectionid == nil { + if limitDesc == nil || limitDesc.LimitId == nil { continue } - var currentLimitsForID features.LoadControlLimitType - var found bool - for _, item := range currentLimits { - if uint(*limitId) != item.LimitId { - continue - } - currentLimitsForID = item - found = true - break - } - if !found || !currentLimitsForID.IsChangeable { + limitIdData, err := e.evLoadControl.GetLimitValueForLimitId(*limitDesc.LimitId) + if err != nil { continue } - limitValue := model.NewScaledNumberType(limit) - for _, elLimit := range elLimits { - if elLimit.ConnectionID != uint(*elConnectionid) { - continue - } - if elLimit.Scope != model.ScopeTypeTypeACCurrent { - continue - } - if limit < elLimit.Min { - limitValue = model.NewScaledNumberType(elLimit.Default) - } - if limit > elLimit.Max { - limitValue = model.NewScaledNumberType(elLimit.Max) - } + // EEBus_UC_TS_OverloadProtectionByEvChargingCurrentCurtailment V1.01b 3.2.1.2.2.2 + // If omitted or set to "true", the timePeriod, value and isLimitActive element SHALL be writeable by a client. + if limitIdData.IsLimitChangeable != nil && !*limitIdData.IsLimitChangeable { + continue } - active := true + // electricalPermittedValueSet contains the allowed min, max and the default values per phase + phaseLimit = e.evElectricalConnection.AdjustValueToBeWithinPermittedValuesForParameter(phaseLimit, *elParamDesc.ParameterId) + newLimit := model.LoadControlLimitDataType{ - LimitId: limitId, - IsLimitActive: &active, - Value: limitValue, + LimitId: limitDesc.LimitId, + IsLimitActive: eebusUtil.Ptr(true), + Value: model.NewScaledNumberType(phaseLimit), } limitData = append(limitData, newLimit) } } - _, err = e.evLoadControl.WriteLimitValues(limitData) + _, err := e.evLoadControl.WriteLimitValues(limitData) return err } @@ -370,20 +439,22 @@ func (e *EMobilityImpl) EVCommunicationStandard() (EVCommunicationStandardType, } // check if device configuration descriptions has an communication standard key name - support, err := e.evDeviceConfiguration.GetDescriptionKeyNameSupport(model.DeviceConfigurationKeyNameTypeCommunicationsStandard) + _, err := e.evDeviceConfiguration.GetDescriptionForKeyName(model.DeviceConfigurationKeyNameTypeCommunicationsStandard) if err != nil { return EVCommunicationStandardTypeUnknown, err } - if !support { - return EVCommunicationStandardTypeUnknown, features.ErrNotSupported - } - data, err := e.evDeviceConfiguration.GetEVCommunicationStandard() + data, err := e.evDeviceConfiguration.GetKeyValueForKeyName(model.DeviceConfigurationKeyNameTypeCommunicationsStandard, model.DeviceConfigurationKeyValueTypeTypeString) if err != nil { return EVCommunicationStandardTypeUnknown, err } - return EVCommunicationStandardType(*data), err + if data == nil { + return EVCommunicationStandardTypeUnknown, features.ErrDataNotAvailable + } + + value := data.(*model.DeviceConfigurationKeyValueStringType) + return EVCommunicationStandardType(*value), nil } // returns the identification of the currently connected EV or nil if not available @@ -406,9 +477,12 @@ func (e *EMobilityImpl) EVIdentification() (string, error) { } for _, identification := range identifications { - if identification.Identifier != "" { - return identification.Identifier, nil + value := identification.IdentificationValue + if value == nil { + continue } + + return string(*value), nil } return "", nil } @@ -419,7 +493,7 @@ func (e *EMobilityImpl) EVIdentification() (string, error) { // - ErrDataNotAvailable if that information is not (yet) available // - and others func (e *EMobilityImpl) EVOptimizationOfSelfConsumptionSupported() (bool, error) { - if e.evEntity == nil || e.evLoadControl == nil { + if e.evEntity == nil { return false, ErrEVDisconnected } @@ -438,11 +512,11 @@ func (e *EMobilityImpl) EVOptimizationOfSelfConsumptionSupported() (bool, error) } // check if loadcontrol limit descriptions contains a recommendation category - support, err := e.evLoadControl.GetLimitDescriptionCategorySupport(model.LoadControlCategoryTypeRecommendation) - if err != nil { + if _, err = e.evLoadControl.GetLimitDescriptionsForCategory(model.LoadControlCategoryTypeRecommendation); err != nil { return false, err } - return support, nil + + return true, nil } // return if the EVSE and EV combination support providing an SoC @@ -474,7 +548,7 @@ func (e *EMobilityImpl) EVSoCSupported() (bool, error) { } // check if measurement descriptions has an SoC scope type - desc, err := e.evMeasurement.GetDescriptionForScope(model.ScopeTypeTypeStateOfCharge) + desc, err := e.evMeasurement.GetDescriptionsForScope(model.ScopeTypeTypeStateOfCharge) if err != nil { return false, err } @@ -497,23 +571,31 @@ func (e *EMobilityImpl) EVSoCSupported() (bool, error) { // - and others func (e *EMobilityImpl) EVSoC() (float64, error) { if e.evEntity == nil { - return 0.0, ErrEVDisconnected + return 0, ErrEVDisconnected } if e.evMeasurement == nil { - return 0.0, features.ErrDataNotAvailable + return 0, features.ErrDataNotAvailable } // check if the SoC is supported support, err := e.EVSoCSupported() if err != nil { - return 0.0, err + return 0, err } if !support { - return 0.0, features.ErrNotSupported + return 0, features.ErrNotSupported + } + + data, err := e.evMeasurement.GetValuesForTypeCommodityScope(model.MeasurementTypeTypePercentage, model.CommodityTypeTypeElectricity, model.ScopeTypeTypeStateOfCharge) + if err != nil { + return 0, err } - return e.evMeasurement.GetSoC() + // we assume there is only one value, nil is already checked + value := data[0].Value + + return value.GetValue(), nil } // returns if the EVSE and EV combination support coordinated charging @@ -538,3 +620,331 @@ func (e *EMobilityImpl) EVCoordinatedChargingSupported() (bool, error) { return true, nil } + +// returns the current charging strategy +func (e *EMobilityImpl) EVChargeStrategy() EVChargeStrategyType { + if e.evEntity == nil || e.evTimeSeries == nil { + return EVChargeStrategyTypeUnknown + } + + // only ISO communication can provide a charging strategy information + com, err := e.EVCommunicationStandard() + if err != nil || com == EVCommunicationStandardTypeUnknown || com == EVCommunicationStandardTypeIEC61851 { + return EVChargeStrategyTypeUnknown + } + + // only the time series data for singledemand is relevant for detecting the charging strategy + data, err := e.evTimeSeries.GetValueForType(model.TimeSeriesTypeTypeSingleDemand) + if err != nil { + return EVChargeStrategyTypeUnknown + } + + // without time series slots, there is no known strategy + if data.TimeSeriesSlot == nil || len(data.TimeSeriesSlot) == 0 { + return EVChargeStrategyTypeUnknown + } + + // get the value for the first slot + firstSlot := data.TimeSeriesSlot[0] + + switch { + case firstSlot.Duration == nil: + // if value is > 0 and duration does not exist, the EV is direct charging + if firstSlot.Value != nil { + return EVChargeStrategyTypeDirectCharging + } + + case firstSlot.Duration != nil: + if _, err := firstSlot.Duration.GetTimeDuration(); err != nil { + // we got an invalid duration + return EVChargeStrategyTypeUnknown + } + + if firstSlot.MinValue != nil && firstSlot.MinValue.GetValue() > 0 { + return EVChargeStrategyTypeMinSoC + } + + if firstSlot.Value != nil { + if firstSlot.Value.GetValue() > 0 { + // there is demand and a duration + return EVChargeStrategyTypeTimedCharging + } + + return EVChargeStrategyTypeNoDemand + } + + } + + return EVChargeStrategyTypeUnknown +} + +// returns the current energy demand in Wh and the duration +func (e *EMobilityImpl) EVEnergyDemand() (EVDemand, error) { + demand := EVDemand{} + + if e.evEntity == nil { + return demand, ErrEVDisconnected + } + + if e.evTimeSeries == nil { + return demand, features.ErrDataNotAvailable + } + + data, err := e.evTimeSeries.GetValueForType(model.TimeSeriesTypeTypeSingleDemand) + if err != nil { + return demand, features.ErrDataNotAvailable + } + + // we need at a time series slot + if data.TimeSeriesSlot == nil { + return demand, features.ErrDataNotAvailable + } + + // get the value for the first slot, ignore all others, which + // in the tests so far always have min/max/value 0 + firstSlot := data.TimeSeriesSlot[0] + if firstSlot.MinValue != nil { + demand.MinDemand = firstSlot.MinValue.GetValue() + } + if firstSlot.Value != nil { + demand.OptDemand = firstSlot.Value.GetValue() + } + if firstSlot.MaxValue != nil { + demand.MaxDemand = firstSlot.MaxValue.GetValue() + } + if firstSlot.Duration != nil { + if tempDuration, err := firstSlot.Duration.GetTimeDuration(); err == nil { + demand.DurationUntilEnd = tempDuration + } + } + + // start time has to be defined either in TimePeriod or the first slot + relStartTime := time.Duration(0) + + startTimeSet := false + if data.TimePeriod != nil && data.TimePeriod.StartTime != nil { + if temp, err := data.TimePeriod.StartTime.GetTimeDuration(); err == nil { + relStartTime = temp + startTimeSet = true + } + } + + if !startTimeSet { + if firstSlot.TimePeriod != nil && firstSlot.TimePeriod.StartTime != nil { + if temp, err := firstSlot.TimePeriod.StartTime.GetTimeDuration(); err == nil { + relStartTime = temp + } + } + } + + demand.DurationUntilStart = relStartTime + + return demand, nil +} + +// returns the constraints for the power slots +func (e *EMobilityImpl) EVGetPowerConstraints() EVTimeSlotConstraints { + result := EVTimeSlotConstraints{} + + if e.evTimeSeries == nil { + return result + } + + constraints, err := e.evTimeSeries.GetConstraints() + if err != nil { + return result + } + + // only use the first constraint + constraint := constraints[0] + + if constraint.SlotCountMin != nil { + result.MinSlots = uint(*constraint.SlotCountMin) + } + if constraint.SlotCountMax != nil { + result.MaxSlots = uint(*constraint.SlotCountMax) + } + if constraint.SlotDurationMin != nil { + if duration, err := constraint.SlotDurationMin.GetTimeDuration(); err == nil { + result.MinSlotDuration = duration + } + } + if constraint.SlotDurationMax != nil { + if duration, err := constraint.SlotDurationMax.GetTimeDuration(); err == nil { + result.MaxSlotDuration = duration + } + } + if constraint.SlotDurationStepSize != nil { + if duration, err := constraint.SlotDurationStepSize.GetTimeDuration(); err == nil { + result.SlotDurationStepSize = duration + } + } + + return result +} + +// send power limits to the EV +func (e *EMobilityImpl) EVWritePowerLimits(data []EVDurationSlotValue) error { + if e.evTimeSeries == nil { + return ErrNotSupported + } + + if len(data) == 0 { + return errors.New("missing power limit data") + } + + constraints := e.EVGetPowerConstraints() + + if constraints.MinSlots != 0 && constraints.MinSlots > uint(len(data)) { + return errors.New("too few charge slots provided") + } + + if constraints.MaxSlots != 0 && constraints.MaxSlots < uint(len(data)) { + return errors.New("too many charge slots provided") + } + + desc, err := e.evTimeSeries.GetDescriptionForType(model.TimeSeriesTypeTypeConstraints) + if err != nil { + return ErrNotSupported + } + + timeSeriesSlots := []model.TimeSeriesSlotType{} + var totalDuration time.Duration + for index, slot := range data { + relativeStart := totalDuration + + timeSeriesSlot := model.TimeSeriesSlotType{ + TimeSeriesSlotId: eebusUtil.Ptr(model.TimeSeriesSlotIdType(index)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeTypeFromDuration(relativeStart), + }, + MaxValue: model.NewScaledNumberType(slot.Value), + } + + // the last slot also needs an End Time + if index == len(data)-1 { + relativeEndTime := relativeStart + slot.Duration + timeSeriesSlot.TimePeriod.EndTime = model.NewAbsoluteOrRelativeTimeTypeFromDuration(relativeEndTime) + } + timeSeriesSlots = append(timeSeriesSlots, timeSeriesSlot) + + totalDuration += slot.Duration + } + + timeSeriesData := model.TimeSeriesDataType{ + TimeSeriesId: desc.TimeSeriesId, + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + EndTime: model.NewAbsoluteOrRelativeTimeTypeFromDuration(totalDuration), + }, + TimeSeriesSlot: timeSeriesSlots, + } + + _, err = e.evTimeSeries.WriteValues([]model.TimeSeriesDataType{timeSeriesData}) + + return err +} + +// returns the minimum and maximum number of incentive slots allowed +func (e *EMobilityImpl) EVGetIncentiveConstraints() EVIncentiveSlotConstraints { + result := EVIncentiveSlotConstraints{} + + if e.evIncentiveTable == nil { + return result + } + + constraints, err := e.evIncentiveTable.GetConstraints() + if err != nil { + return result + } + + // only use the first constraint + constraint := constraints[0] + + if constraint.IncentiveSlotConstraints.SlotCountMin != nil { + result.MinSlots = uint(*constraint.IncentiveSlotConstraints.SlotCountMin) + } + if constraint.IncentiveSlotConstraints.SlotCountMax != nil { + result.MaxSlots = uint(*constraint.IncentiveSlotConstraints.SlotCountMax) + } + + return result +} + +// send incentives to the EV +func (e *EMobilityImpl) EVWriteIncentives(data []EVDurationSlotValue) error { + if e.evIncentiveTable == nil { + return features.ErrDataNotAvailable + } + + if len(data) == 0 { + return errors.New("missing incentive data") + } + + constraints := e.EVGetIncentiveConstraints() + + if constraints.MinSlots != 0 && constraints.MinSlots > uint(len(data)) { + return errors.New("too few charge slots provided") + } + + if constraints.MaxSlots != 0 && constraints.MaxSlots < uint(len(data)) { + return errors.New("too many charge slots provided") + } + + incentiveSlots := []model.IncentiveTableIncentiveSlotType{} + var totalDuration time.Duration + for index, slot := range data { + relativeStart := totalDuration + + timeInterval := &model.TimeTableDataType{ + StartTime: &model.AbsoluteOrRecurringTimeType{ + Relative: model.NewDurationType(relativeStart), + }, + } + + // the last slot also needs an End Time + if index == len(data)-1 { + relativeEndTime := relativeStart + slot.Duration + timeInterval.EndTime = &model.AbsoluteOrRecurringTimeType{ + Relative: model.NewDurationType(relativeEndTime), + } + } + + incentiveSlot := model.IncentiveTableIncentiveSlotType{ + TimeInterval: timeInterval, + Tier: []model.IncentiveTableTierType{ + { + Tier: &model.TierDataType{ + TierId: eebusUtil.Ptr(model.TierIdType(1)), + }, + Boundary: []model.TierBoundaryDataType{ + { + BoundaryId: eebusUtil.Ptr(model.TierBoundaryIdType(1)), // only 1 boundary exists + LowerBoundaryValue: model.NewScaledNumberType(0), + }, + }, + Incentive: []model.IncentiveDataType{ + { + IncentiveId: eebusUtil.Ptr(model.IncentiveIdType(1)), // always use price + Value: model.NewScaledNumberType(slot.Value), + }, + }, + }, + }, + } + incentiveSlots = append(incentiveSlots, incentiveSlot) + + totalDuration += slot.Duration + } + + incentiveData := model.IncentiveTableType{ + Tariff: &model.TariffDataType{ + TariffId: eebusUtil.Ptr(model.TariffIdType(0)), + }, + IncentiveSlot: incentiveSlots, + } + + _, err := e.evIncentiveTable.WriteValues([]model.IncentiveTableType{incentiveData}) + + return err +} diff --git a/emobility/public_EVChargeStrategy_test.go b/emobility/public_EVChargeStrategy_test.go new file mode 100644 index 0000000..1602ef0 --- /dev/null +++ b/emobility/public_EVChargeStrategy_test.go @@ -0,0 +1,199 @@ +package emobility + +import ( + "testing" + "time" + + "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/eebus-go/util" + "github.com/stretchr/testify/assert" +) + +func Test_EVChargeStrategy(t *testing.T) { + emobilty, eebusService := setupEmobility() + + data := emobilty.EVChargeStrategy() + assert.Equal(t, EVChargeStrategyTypeUnknown, data) + + localDevice, remoteDevice, entites, _ := setupDevices(eebusService) + emobilty.evseEntity = entites[0] + emobilty.evEntity = entites[1] + + data = emobilty.EVChargeStrategy() + assert.Equal(t, EVChargeStrategyTypeUnknown, data) + + emobilty.evDeviceConfiguration = deviceConfiguration(localDevice, emobilty.evEntity) + + data = emobilty.EVChargeStrategy() + assert.Equal(t, EVChargeStrategyTypeUnknown, data) + + datagram := datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer, model.RoleTypeClient) + + cmd := []model.CmdType{{ + DeviceConfigurationKeyValueDescriptionListData: &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeCommunicationsStandard), + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err := localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data = emobilty.EVChargeStrategy() + assert.Equal(t, EVChargeStrategyTypeUnknown, data) + + cmd = []model.CmdType{{ + DeviceConfigurationKeyValueListData: &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + String: util.Ptr(model.DeviceConfigurationKeyValueStringType(EVCommunicationStandardTypeISO151182ED1)), + }, + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data = emobilty.EVChargeStrategy() + assert.Equal(t, EVChargeStrategyTypeUnknown, data) + + emobilty.evTimeSeries = timeSeriesConfiguration(localDevice, emobilty.evEntity) + + data = emobilty.EVChargeStrategy() + assert.Equal(t, EVChargeStrategyTypeUnknown, data) + + datagram = datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeServer, model.RoleTypeClient) + + cmd = []model.CmdType{{ + TimeSeriesDescriptionListData: &model.TimeSeriesDescriptionListDataType{ + TimeSeriesDescriptionData: []model.TimeSeriesDescriptionDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeSingleDemand), + }, + }, + }}} + + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + cmd = []model.CmdType{{ + TimeSeriesListData: &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + }, + }, + }}} + + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data = emobilty.EVChargeStrategy() + assert.Equal(t, EVChargeStrategyTypeUnknown, data) + + cmd = []model.CmdType{{ + TimeSeriesListData: &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + }, + }, + }, + }, + }}} + + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data = emobilty.EVChargeStrategy() + assert.Equal(t, EVChargeStrategyTypeUnknown, data) + + cmd = []model.CmdType{{ + TimeSeriesListData: &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + Duration: util.Ptr(model.DurationType("PT0S")), + Value: model.NewScaledNumberType(0), + }, + }, + }, + }, + }}} + + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data = emobilty.EVChargeStrategy() + assert.Equal(t, EVChargeStrategyTypeNoDemand, data) + + cmd = []model.CmdType{{ + TimeSeriesListData: &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + Value: model.NewScaledNumberType(10000), + }, + }, + }, + }, + }}} + + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data = emobilty.EVChargeStrategy() + assert.Equal(t, EVChargeStrategyTypeDirectCharging, data) + + cmd = []model.CmdType{{ + TimeSeriesListData: &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + Value: model.NewScaledNumberType(10000), + Duration: model.NewDurationType(2 * time.Hour), + }, + }, + }, + }, + }}} + + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data = emobilty.EVChargeStrategy() + assert.Equal(t, EVChargeStrategyTypeTimedCharging, data) +} diff --git a/emobility/public_EVChargedEnergy_test.go b/emobility/public_EVChargedEnergy_test.go new file mode 100644 index 0000000..dfede96 --- /dev/null +++ b/emobility/public_EVChargedEnergy_test.go @@ -0,0 +1,72 @@ +package emobility + +import ( + "testing" + + "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/eebus-go/util" + "github.com/stretchr/testify/assert" +) + +func Test_EVChargedEnergy(t *testing.T) { + emobilty, eebusService := setupEmobility() + + data, err := emobilty.EVChargedEnergy() + assert.NotNil(t, err) + assert.Equal(t, 0.0, data) + + localDevice, remoteDevice, entites, _ := setupDevices(eebusService) + emobilty.evseEntity = entites[0] + emobilty.evEntity = entites[1] + + data, err = emobilty.EVChargedEnergy() + assert.NotNil(t, err) + assert.Equal(t, 0.0, data) + + emobilty.evMeasurement = measurement(localDevice, emobilty.evEntity) + + data, err = emobilty.EVChargedEnergy() + assert.NotNil(t, err) + assert.Equal(t, 0.0, data) + + datagram := datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer, model.RoleTypeClient) + + cmd := []model.CmdType{{ + MeasurementDescriptionListData: &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeCharge), + }, + }, + }}} + + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVChargedEnergy() + assert.NotNil(t, err) + assert.Equal(t, 0.0, data) + + cmd = []model.CmdType{{ + MeasurementListData: &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(80), + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVChargedEnergy() + assert.Nil(t, err) + assert.Equal(t, 80.0, data) +} diff --git a/emobility/public_EVCommunicationStandard_test.go b/emobility/public_EVCommunicationStandard_test.go new file mode 100644 index 0000000..9bfdf1b --- /dev/null +++ b/emobility/public_EVCommunicationStandard_test.go @@ -0,0 +1,89 @@ +package emobility + +import ( + "testing" + + "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/eebus-go/util" + "github.com/stretchr/testify/assert" +) + +func Test_EVCommunicationStandard(t *testing.T) { + emobilty, eebusService := setupEmobility() + + data, err := emobilty.EVCommunicationStandard() + assert.NotNil(t, err) + assert.Equal(t, EVCommunicationStandardTypeUnknown, data) + + localDevice, remoteDevice, entites, _ := setupDevices(eebusService) + emobilty.evseEntity = entites[0] + emobilty.evEntity = entites[1] + + data, err = emobilty.EVCommunicationStandard() + assert.NotNil(t, err) + assert.Equal(t, EVCommunicationStandardTypeUnknown, data) + + emobilty.evDeviceConfiguration = deviceConfiguration(localDevice, emobilty.evEntity) + + data, err = emobilty.EVCommunicationStandard() + assert.NotNil(t, err) + assert.Equal(t, EVCommunicationStandardTypeUnknown, data) + + datagram := datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer, model.RoleTypeClient) + + cmd := []model.CmdType{{ + DeviceConfigurationKeyValueDescriptionListData: &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeAsymmetricChargingSupported), + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVCommunicationStandard() + assert.NotNil(t, err) + assert.Equal(t, EVCommunicationStandardTypeUnknown, data) + + cmd = []model.CmdType{{ + DeviceConfigurationKeyValueDescriptionListData: &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeCommunicationsStandard), + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVCommunicationStandard() + assert.NotNil(t, err) + assert.Equal(t, EVCommunicationStandardTypeUnknown, data) + + cmd = []model.CmdType{{ + DeviceConfigurationKeyValueListData: &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + String: util.Ptr(model.DeviceConfigurationKeyValueStringType(EVCommunicationStandardTypeISO151182ED1)), + }, + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVCommunicationStandard() + assert.Nil(t, err) + assert.Equal(t, EVCommunicationStandardTypeISO151182ED1, data) +} diff --git a/emobility/public_EVConnectedPhases_test.go b/emobility/public_EVConnectedPhases_test.go new file mode 100644 index 0000000..1f24f80 --- /dev/null +++ b/emobility/public_EVConnectedPhases_test.go @@ -0,0 +1,68 @@ +package emobility + +import ( + "testing" + + "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/eebus-go/util" + "github.com/stretchr/testify/assert" +) + +func Test_EVConnectedPhases(t *testing.T) { + emobilty, eebusService := setupEmobility() + + data, err := emobilty.EVConnectedPhases() + assert.NotNil(t, err) + assert.Equal(t, uint(0), data) + + localDevice, remoteDevice, entites, _ := setupDevices(eebusService) + emobilty.evseEntity = entites[0] + emobilty.evEntity = entites[1] + + data, err = emobilty.EVConnectedPhases() + assert.NotNil(t, err) + assert.Equal(t, uint(0), data) + + emobilty.evElectricalConnection = electricalConnection(localDevice, emobilty.evEntity) + + data, err = emobilty.EVConnectedPhases() + assert.NotNil(t, err) + assert.Equal(t, uint(0), data) + + datagram := datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer, model.RoleTypeClient) + + cmd := []model.CmdType{{ + ElectricalConnectionDescriptionListData: &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVConnectedPhases() + assert.Nil(t, err) + assert.Equal(t, uint(3), data) + + cmd = []model.CmdType{{ + ElectricalConnectionDescriptionListData: &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + AcConnectedPhases: util.Ptr(uint(1)), + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVConnectedPhases() + assert.Nil(t, err) + assert.Equal(t, uint(1), data) +} diff --git a/emobility/public_EVCoordinatedChargingSupported_test.go b/emobility/public_EVCoordinatedChargingSupported_test.go new file mode 100644 index 0000000..cd8b1a4 --- /dev/null +++ b/emobility/public_EVCoordinatedChargingSupported_test.go @@ -0,0 +1,51 @@ +package emobility + +import ( + "testing" + + "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/eebus-go/util" + "github.com/stretchr/testify/assert" +) + +func Test_EVCoordinatedChargingSupported(t *testing.T) { + emobilty, eebusService := setupEmobility() + + data, err := emobilty.EVCoordinatedChargingSupported() + assert.NotNil(t, err) + assert.Equal(t, false, data) + + localDevice, remoteDevice, entites, _ := setupDevices(eebusService) + emobilty.evseEntity = entites[0] + emobilty.evEntity = entites[1] + + data, err = emobilty.EVCoordinatedChargingSupported() + assert.Nil(t, err) + assert.Equal(t, false, data) + + datagram := datagramForEntityAndFeatures(true, localDevice, nil, model.FeatureTypeTypeNodeManagement, model.RoleTypeSpecial, model.RoleTypeSpecial) + + cmd := []model.CmdType{{ + NodeManagementUseCaseData: &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: util.Ptr(model.UseCaseActorTypeEV), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: util.Ptr(model.UseCaseNameTypeCoordinatedEVCharging), + UseCaseAvailable: util.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1}, + }, + }, + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVCoordinatedChargingSupported() + assert.Nil(t, err) + assert.Equal(t, true, data) +} diff --git a/emobility/public_EVCurrentChargeState_test.go b/emobility/public_EVCurrentChargeState_test.go new file mode 100644 index 0000000..de37691 --- /dev/null +++ b/emobility/public_EVCurrentChargeState_test.go @@ -0,0 +1,98 @@ +package emobility + +import ( + "testing" + + "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/eebus-go/util" + "github.com/stretchr/testify/assert" +) + +func Test_EVCurrentChargeState(t *testing.T) { + emobilty, eebusService := setupEmobility() + + data, err := emobilty.EVCurrentChargeState() + assert.Nil(t, err) + assert.Equal(t, EVChargeStateTypeUnplugged, data) + + localDevice, remoteDevice, entites, _ := setupDevices(eebusService) + emobilty.evseEntity = entites[0] + emobilty.evEntity = entites[1] + + data, err = emobilty.EVCurrentChargeState() + assert.NotNil(t, err) + assert.Equal(t, EVChargeStateTypeUnknown, data) + + emobilty.evDeviceDiagnosis = deviceDiagnosis(localDevice, emobilty.evEntity) + + data, err = emobilty.EVCurrentChargeState() + assert.NotNil(t, err) + assert.Equal(t, EVChargeStateTypeUnknown, data) + + datagram := datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer, model.RoleTypeClient) + + cmd := []model.CmdType{{ + DeviceDiagnosisStateData: &model.DeviceDiagnosisStateDataType{ + OperatingState: util.Ptr(model.DeviceDiagnosisOperatingStateTypeNormalOperation), + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVCurrentChargeState() + assert.Nil(t, err) + assert.Equal(t, EVChargeStateTypeActive, data) + + cmd = []model.CmdType{{ + DeviceDiagnosisStateData: &model.DeviceDiagnosisStateDataType{ + OperatingState: util.Ptr(model.DeviceDiagnosisOperatingStateTypeStandby), + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVCurrentChargeState() + assert.Nil(t, err) + assert.Equal(t, EVChargeStateTypePaused, data) + + cmd = []model.CmdType{{ + DeviceDiagnosisStateData: &model.DeviceDiagnosisStateDataType{ + OperatingState: util.Ptr(model.DeviceDiagnosisOperatingStateTypeFailure), + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVCurrentChargeState() + assert.Nil(t, err) + assert.Equal(t, EVChargeStateTypeError, data) + + cmd = []model.CmdType{{ + DeviceDiagnosisStateData: &model.DeviceDiagnosisStateDataType{ + OperatingState: util.Ptr(model.DeviceDiagnosisOperatingStateTypeFinished), + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVCurrentChargeState() + assert.Nil(t, err) + assert.Equal(t, EVChargeStateTypeFinished, data) + + cmd = []model.CmdType{{ + DeviceDiagnosisStateData: &model.DeviceDiagnosisStateDataType{ + OperatingState: util.Ptr(model.DeviceDiagnosisOperatingStateTypeInAlarm), + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVCurrentChargeState() + assert.Nil(t, err) + assert.Equal(t, EVChargeStateTypeUnknown, data) +} diff --git a/emobility/public_EVCurrentLimits_test.go b/emobility/public_EVCurrentLimits_test.go new file mode 100644 index 0000000..7dfc77b --- /dev/null +++ b/emobility/public_EVCurrentLimits_test.go @@ -0,0 +1,179 @@ +package emobility + +import ( + "testing" + + "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/eebus-go/util" + "github.com/stretchr/testify/assert" +) + +func Test_EVCurrentLimits(t *testing.T) { + emobilty, eebusService := setupEmobility() + + minData, maxData, defaultData, err := emobilty.EVCurrentLimits() + assert.NotNil(t, err) + assert.Nil(t, minData) + assert.Nil(t, maxData) + assert.Nil(t, defaultData) + + localDevice, remoteDevice, entites, _ := setupDevices(eebusService) + emobilty.evseEntity = entites[0] + emobilty.evEntity = entites[1] + + minData, maxData, defaultData, err = emobilty.EVCurrentLimits() + assert.NotNil(t, err) + assert.Nil(t, minData) + assert.Nil(t, maxData) + assert.Nil(t, defaultData) + + emobilty.evElectricalConnection = electricalConnection(localDevice, emobilty.evEntity) + + minData, maxData, defaultData, err = emobilty.EVCurrentLimits() + assert.NotNil(t, err) + assert.Nil(t, minData) + assert.Nil(t, maxData) + assert.Nil(t, defaultData) + + datagram := datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer, model.RoleTypeClient) + + cmd := []model.CmdType{{ + ElectricalConnectionParameterDescriptionListData: &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(1)), + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(2)), + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeC), + }, + }, + }}} + + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + minData, maxData, defaultData, err = emobilty.EVCurrentLimits() + assert.NotNil(t, err) + assert.Nil(t, minData) + assert.Nil(t, maxData) + assert.Nil(t, defaultData) + + type permittedStruct struct { + defaultExists bool + defaultValue, expectedDefaultValue float64 + minValue, expectedMinValue float64 + maxValue, expectedMaxValue float64 + } + + tests := []struct { + name string + permitted []permittedStruct + }{ + { + "1 Phase ISO15118", + []permittedStruct{ + {true, 0.1, 0.1, 2, 2.2, 16, 16}, + }, + }, + { + "1 Phase IEC61851", + []permittedStruct{ + {true, 0.0, 0.0, 6, 6, 16, 16}, + }, + }, + { + "1 Phase IEC61851 Elli", + []permittedStruct{ + {false, 0.0, 0.0, 6, 6, 16, 16}, + }, + }, + { + "3 Phase ISO15118", + []permittedStruct{ + {true, 0.1, 0.1, 2, 2.2, 16, 16}, + {true, 0.1, 0.1, 2, 2.2, 16, 16}, + {true, 0.1, 0.1, 2, 2.2, 16, 16}, + }, + }, + { + "3 Phase IEC61851", + []permittedStruct{ + {true, 0.0, 0.0, 6, 6, 16, 16}, + {true, 0.0, 0.0, 6, 6, 16, 16}, + {true, 0.0, 0.0, 6, 6, 16, 16}, + }, + }, + { + "3 Phase IEC61851 Elli", + []permittedStruct{ + {false, 0.0, 0.0, 6, 6, 16, 16}, + {false, 0.0, 0.0, 6, 6, 16, 16}, + {false, 0.0, 0.0, 6, 6, 16, 16}, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + dataSet := []model.ElectricalConnectionPermittedValueSetDataType{} + permittedData := []model.ScaledNumberSetType{} + for index, data := range tc.permitted { + item := model.ScaledNumberSetType{ + Range: []model.ScaledNumberRangeType{ + { + Min: model.NewScaledNumberType(data.minValue), + Max: model.NewScaledNumberType(data.maxValue), + }, + }, + } + if data.defaultExists { + item.Value = []model.ScaledNumberType{*model.NewScaledNumberType(data.defaultValue)} + } + permittedData = append(permittedData, item) + + permittedItem := model.ElectricalConnectionPermittedValueSetDataType{ + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(index)), + PermittedValueSet: permittedData, + } + dataSet = append(dataSet, permittedItem) + } + + cmd = []model.CmdType{{ + ElectricalConnectionPermittedValueSetListData: &model.ElectricalConnectionPermittedValueSetListDataType{ + ElectricalConnectionPermittedValueSetData: dataSet, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + minData, maxData, defaultData, err = emobilty.EVCurrentLimits() + assert.Nil(t, err) + + assert.Nil(t, err) + assert.Equal(t, len(tc.permitted), len(minData)) + assert.Equal(t, len(tc.permitted), len(maxData)) + assert.Equal(t, len(tc.permitted), len(defaultData)) + for index, item := range tc.permitted { + assert.Equal(t, item.expectedMinValue, minData[index]) + assert.Equal(t, item.expectedMaxValue, maxData[index]) + assert.Equal(t, item.expectedDefaultValue, defaultData[index]) + } + }) + } +} diff --git a/emobility/public_EVCurrentsPerPhase_test.go b/emobility/public_EVCurrentsPerPhase_test.go new file mode 100644 index 0000000..eeeb407 --- /dev/null +++ b/emobility/public_EVCurrentsPerPhase_test.go @@ -0,0 +1,97 @@ +package emobility + +import ( + "testing" + + "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/eebus-go/util" + "github.com/stretchr/testify/assert" +) + +func Test_EVCurrentsPerPhase(t *testing.T) { + emobilty, eebusService := setupEmobility() + + data, err := emobilty.EVCurrentsPerPhase() + assert.NotNil(t, err) + assert.Nil(t, data) + + localDevice, remoteDevice, entites, _ := setupDevices(eebusService) + emobilty.evseEntity = entites[0] + emobilty.evEntity = entites[1] + + data, err = emobilty.EVCurrentsPerPhase() + assert.NotNil(t, err) + assert.Nil(t, data) + + emobilty.evElectricalConnection = electricalConnection(localDevice, emobilty.evEntity) + emobilty.evMeasurement = measurement(localDevice, emobilty.evEntity) + + data, err = emobilty.EVCurrentsPerPhase() + assert.NotNil(t, err) + assert.Nil(t, data) + + datagram := datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer, model.RoleTypeClient) + + cmd := []model.CmdType{{ + ElectricalConnectionParameterDescriptionListData: &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeACCurrent), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + }, + }}} + + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVPowerPerPhase() + assert.NotNil(t, err) + assert.Nil(t, data) + + datagram = datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer, model.RoleTypeClient) + + cmd = []model.CmdType{{ + MeasurementDescriptionListData: &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeCurrent), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACCurrent), + }, + }, + }}} + + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVCurrentsPerPhase() + assert.NotNil(t, err) + assert.Nil(t, data) + + cmd = []model.CmdType{{ + MeasurementListData: &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVCurrentsPerPhase() + assert.Nil(t, err) + assert.Equal(t, 10.0, data[0]) +} diff --git a/emobility/public_EVEnergyDemand_test.go b/emobility/public_EVEnergyDemand_test.go new file mode 100644 index 0000000..82b6af8 --- /dev/null +++ b/emobility/public_EVEnergyDemand_test.go @@ -0,0 +1,236 @@ +package emobility + +import ( + "testing" + "time" + + "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/eebus-go/util" + "github.com/stretchr/testify/assert" +) + +func Test_EVEnergySingleDemand(t *testing.T) { + emobilty, eebusService := setupEmobility() + + demand, err := emobilty.EVEnergyDemand() + assert.NotNil(t, err) + assert.Equal(t, 0.0, demand.MinDemand) + assert.Equal(t, 0.0, demand.OptDemand) + assert.Equal(t, 0.0, demand.MaxDemand) + assert.Equal(t, time.Duration(0), demand.DurationUntilStart) + assert.Equal(t, time.Duration(0), demand.DurationUntilEnd) + + localDevice, remoteDevice, entites, _ := setupDevices(eebusService) + emobilty.evseEntity = entites[0] + emobilty.evEntity = entites[1] + + demand, err = emobilty.EVEnergyDemand() + assert.NotNil(t, err) + assert.Equal(t, 0.0, demand.MinDemand) + assert.Equal(t, 0.0, demand.OptDemand) + assert.Equal(t, 0.0, demand.MaxDemand) + assert.Equal(t, time.Duration(0), demand.DurationUntilStart) + assert.Equal(t, time.Duration(0), demand.DurationUntilEnd) + + emobilty.evDeviceConfiguration = deviceConfiguration(localDevice, emobilty.evEntity) + + demand, err = emobilty.EVEnergyDemand() + assert.NotNil(t, err) + assert.Equal(t, 0.0, demand.MinDemand) + assert.Equal(t, 0.0, demand.OptDemand) + assert.Equal(t, 0.0, demand.MaxDemand) + assert.Equal(t, time.Duration(0), demand.DurationUntilStart) + assert.Equal(t, time.Duration(0), demand.DurationUntilEnd) + + datagram := datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer, model.RoleTypeClient) + + cmd := []model.CmdType{{ + DeviceConfigurationKeyValueDescriptionListData: &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeCommunicationsStandard), + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + demand, err = emobilty.EVEnergyDemand() + assert.NotNil(t, err) + assert.Equal(t, 0.0, demand.MinDemand) + assert.Equal(t, 0.0, demand.OptDemand) + assert.Equal(t, 0.0, demand.MaxDemand) + assert.Equal(t, time.Duration(0), demand.DurationUntilStart) + assert.Equal(t, time.Duration(0), demand.DurationUntilEnd) + + cmd = []model.CmdType{{ + DeviceConfigurationKeyValueListData: &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + String: util.Ptr(model.DeviceConfigurationKeyValueStringType(EVCommunicationStandardTypeISO151182ED1)), + }, + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + demand, err = emobilty.EVEnergyDemand() + assert.NotNil(t, err) + assert.Equal(t, 0.0, demand.MinDemand) + assert.Equal(t, 0.0, demand.OptDemand) + assert.Equal(t, 0.0, demand.MaxDemand) + assert.Equal(t, time.Duration(0), demand.DurationUntilStart) + assert.Equal(t, time.Duration(0), demand.DurationUntilEnd) + + emobilty.evTimeSeries = timeSeriesConfiguration(localDevice, emobilty.evEntity) + + demand, err = emobilty.EVEnergyDemand() + assert.NotNil(t, err) + assert.Equal(t, 0.0, demand.MinDemand) + assert.Equal(t, 0.0, demand.OptDemand) + assert.Equal(t, 0.0, demand.MaxDemand) + assert.Equal(t, time.Duration(0), demand.DurationUntilStart) + assert.Equal(t, time.Duration(0), demand.DurationUntilEnd) + + datagram = datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeServer, model.RoleTypeClient) + + cmd = []model.CmdType{{ + TimeSeriesDescriptionListData: &model.TimeSeriesDescriptionListDataType{ + TimeSeriesDescriptionData: []model.TimeSeriesDescriptionDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeSingleDemand), + }, + }, + }}} + + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + cmd = []model.CmdType{{ + TimeSeriesListData: &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + }, + }, + }}} + + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + demand, err = emobilty.EVEnergyDemand() + assert.NotNil(t, err) + assert.Equal(t, 0.0, demand.MinDemand) + assert.Equal(t, 0.0, demand.OptDemand) + assert.Equal(t, 0.0, demand.MaxDemand) + assert.Equal(t, time.Duration(0), demand.DurationUntilStart) + assert.Equal(t, time.Duration(0), demand.DurationUntilEnd) + + cmd = []model.CmdType{{ + TimeSeriesListData: &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + }, + }, + }, + }, + }}} + + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + demand, err = emobilty.EVEnergyDemand() + assert.Nil(t, err) + assert.Equal(t, 0.0, demand.MinDemand) + assert.Equal(t, 0.0, demand.OptDemand) + assert.Equal(t, 0.0, demand.MaxDemand) + assert.Equal(t, time.Duration(0), demand.DurationUntilStart) + assert.Equal(t, time.Duration(0), demand.DurationUntilEnd) + + cmd = []model.CmdType{{ + TimeSeriesListData: &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + MinValue: model.NewScaledNumberType(1000), + Value: model.NewScaledNumberType(10000), + MaxValue: model.NewScaledNumberType(100000), + }, + }, + }, + }, + }}} + + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + demand, err = emobilty.EVEnergyDemand() + assert.Nil(t, err) + assert.Equal(t, 1000.0, demand.MinDemand) + assert.Equal(t, 10000.0, demand.OptDemand) + assert.Equal(t, 100000.0, demand.MaxDemand) + assert.Equal(t, time.Duration(0), demand.DurationUntilStart) + assert.Equal(t, time.Duration(0), demand.DurationUntilEnd) + + cmd = []model.CmdType{{ + TimeSeriesListData: &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + Value: model.NewScaledNumberType(10000), + Duration: model.NewDurationType(2 * time.Hour), + }, + }, + }, + }, + }}} + + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + demand, err = emobilty.EVEnergyDemand() + assert.Nil(t, err) + assert.Equal(t, 0.0, demand.MinDemand) + assert.Equal(t, 10000.0, demand.OptDemand) + assert.Equal(t, 0.0, demand.MaxDemand) + assert.Equal(t, time.Duration(0), demand.DurationUntilStart) + assert.Equal(t, time.Duration(2*time.Hour), demand.DurationUntilEnd) +} diff --git a/emobility/public_EVGetIncentiveConstraints_test.go b/emobility/public_EVGetIncentiveConstraints_test.go new file mode 100644 index 0000000..07f3f0b --- /dev/null +++ b/emobility/public_EVGetIncentiveConstraints_test.go @@ -0,0 +1,75 @@ +package emobility + +import ( + "testing" + + "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/eebus-go/util" + "github.com/stretchr/testify/assert" +) + +func Test_EVGetIncentiveConstraints(t *testing.T) { + emobilty, eebusService := setupEmobility() + + constraints := emobilty.EVGetIncentiveConstraints() + assert.Equal(t, uint(0), constraints.MinSlots) + assert.Equal(t, uint(0), constraints.MaxSlots) + + localDevice, remoteDevice, entites, _ := setupDevices(eebusService) + emobilty.evseEntity = entites[0] + emobilty.evEntity = entites[1] + + constraints = emobilty.EVGetIncentiveConstraints() + assert.Equal(t, uint(0), constraints.MinSlots) + assert.Equal(t, uint(0), constraints.MaxSlots) + + emobilty.evIncentiveTable = incentiveTableConfiguration(localDevice, emobilty.evEntity) + + constraints = emobilty.EVGetIncentiveConstraints() + assert.Equal(t, uint(0), constraints.MinSlots) + assert.Equal(t, uint(0), constraints.MaxSlots) + + datagram := datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeIncentiveTable, model.RoleTypeServer, model.RoleTypeClient) + + cmd := []model.CmdType{{ + IncentiveTableConstraintsData: &model.IncentiveTableConstraintsDataType{ + IncentiveTableConstraints: []model.IncentiveTableConstraintsType{ + { + IncentiveSlotConstraints: &model.TimeTableConstraintsDataType{ + SlotCountMin: util.Ptr(model.TimeSlotCountType(1)), + SlotCountMax: util.Ptr(model.TimeSlotCountType(10)), + }, + }, + }, + }}} + + datagram.Payload.Cmd = cmd + + err := localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + constraints = emobilty.EVGetIncentiveConstraints() + assert.Equal(t, uint(1), constraints.MinSlots) + assert.Equal(t, uint(10), constraints.MaxSlots) + + cmd = []model.CmdType{{ + IncentiveTableConstraintsData: &model.IncentiveTableConstraintsDataType{ + IncentiveTableConstraints: []model.IncentiveTableConstraintsType{ + { + IncentiveSlotConstraints: &model.TimeTableConstraintsDataType{ + SlotCountMin: util.Ptr(model.TimeSlotCountType(1)), + }, + }, + }, + }}} + + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + constraints = emobilty.EVGetIncentiveConstraints() + assert.Equal(t, uint(1), constraints.MinSlots) + assert.Equal(t, uint(0), constraints.MaxSlots) + +} diff --git a/emobility/public_EVGetPowerConstraints_test.go b/emobility/public_EVGetPowerConstraints_test.go new file mode 100644 index 0000000..8fe7b26 --- /dev/null +++ b/emobility/public_EVGetPowerConstraints_test.go @@ -0,0 +1,69 @@ +package emobility + +import ( + "testing" + "time" + + "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/eebus-go/util" + "github.com/stretchr/testify/assert" +) + +func Test_EVGetPowerConstraints(t *testing.T) { + emobilty, eebusService := setupEmobility() + + constraints := emobilty.EVGetPowerConstraints() + assert.Equal(t, uint(0), constraints.MinSlots) + assert.Equal(t, uint(0), constraints.MaxSlots) + assert.Equal(t, time.Duration(0), constraints.MinSlotDuration) + assert.Equal(t, time.Duration(0), constraints.MaxSlotDuration) + assert.Equal(t, time.Duration(0), constraints.SlotDurationStepSize) + + localDevice, remoteDevice, entites, _ := setupDevices(eebusService) + emobilty.evseEntity = entites[0] + emobilty.evEntity = entites[1] + + constraints = emobilty.EVGetPowerConstraints() + assert.Equal(t, uint(0), constraints.MinSlots) + assert.Equal(t, uint(0), constraints.MaxSlots) + assert.Equal(t, time.Duration(0), constraints.MinSlotDuration) + assert.Equal(t, time.Duration(0), constraints.MaxSlotDuration) + assert.Equal(t, time.Duration(0), constraints.SlotDurationStepSize) + + emobilty.evTimeSeries = timeSeriesConfiguration(localDevice, emobilty.evEntity) + + constraints = emobilty.EVGetPowerConstraints() + assert.Equal(t, uint(0), constraints.MinSlots) + assert.Equal(t, uint(0), constraints.MaxSlots) + assert.Equal(t, time.Duration(0), constraints.MinSlotDuration) + assert.Equal(t, time.Duration(0), constraints.MaxSlotDuration) + assert.Equal(t, time.Duration(0), constraints.SlotDurationStepSize) + + datagram := datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeServer, model.RoleTypeClient) + + cmd := []model.CmdType{{ + TimeSeriesConstraintsListData: &model.TimeSeriesConstraintsListDataType{ + TimeSeriesConstraintsData: []model.TimeSeriesConstraintsDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + SlotCountMin: util.Ptr(model.TimeSeriesSlotCountType(1)), + SlotCountMax: util.Ptr(model.TimeSeriesSlotCountType(10)), + SlotDurationMin: model.NewDurationType(1 * time.Minute), + SlotDurationMax: model.NewDurationType(60 * time.Minute), + SlotDurationStepSize: model.NewDurationType(1 * time.Minute), + }, + }, + }}} + + datagram.Payload.Cmd = cmd + + err := localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + constraints = emobilty.EVGetPowerConstraints() + assert.Equal(t, uint(1), constraints.MinSlots) + assert.Equal(t, uint(10), constraints.MaxSlots) + assert.Equal(t, time.Duration(1*time.Minute), constraints.MinSlotDuration) + assert.Equal(t, time.Duration(1*time.Hour), constraints.MaxSlotDuration) + assert.Equal(t, time.Duration(1*time.Minute), constraints.SlotDurationStepSize) +} diff --git a/emobility/public_EVIdentification_test.go b/emobility/public_EVIdentification_test.go new file mode 100644 index 0000000..a2ea394 --- /dev/null +++ b/emobility/public_EVIdentification_test.go @@ -0,0 +1,52 @@ +package emobility + +import ( + "testing" + + "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/eebus-go/util" + "github.com/stretchr/testify/assert" +) + +func Test_EVIdentification(t *testing.T) { + emobilty, eebusService := setupEmobility() + + data, err := emobilty.EVIdentification() + assert.NotNil(t, err) + assert.Equal(t, "", data) + + localDevice, remoteDevice, entites, _ := setupDevices(eebusService) + emobilty.evseEntity = entites[0] + emobilty.evEntity = entites[1] + + data, err = emobilty.EVIdentification() + assert.NotNil(t, err) + assert.Equal(t, "", data) + + emobilty.evIdentification = identificationConfiguration(localDevice, emobilty.evEntity) + + data, err = emobilty.EVIdentification() + assert.NotNil(t, err) + assert.Equal(t, "", data) + + datagram := datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeIdentification, model.RoleTypeServer, model.RoleTypeClient) + + cmd := []model.CmdType{{ + IdentificationListData: &model.IdentificationListDataType{ + IdentificationData: []model.IdentificationDataType{ + { + IdentificationId: util.Ptr(model.IdentificationIdType(0)), + IdentificationType: util.Ptr(model.IdentificationTypeTypeEui64), + IdentificationValue: util.Ptr(model.IdentificationValueType("test")), + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVIdentification() + assert.Nil(t, err) + assert.Equal(t, "test", data) +} diff --git a/emobility/public_EVOptimizationOfSelfConsumptionSupported_test.go b/emobility/public_EVOptimizationOfSelfConsumptionSupported_test.go new file mode 100644 index 0000000..3370d39 --- /dev/null +++ b/emobility/public_EVOptimizationOfSelfConsumptionSupported_test.go @@ -0,0 +1,77 @@ +package emobility + +import ( + "testing" + + "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/eebus-go/util" + "github.com/stretchr/testify/assert" +) + +func Test_EVOptimizationOfSelfConsumptionSupported(t *testing.T) { + emobilty, eebusService := setupEmobility() + + data, err := emobilty.EVOptimizationOfSelfConsumptionSupported() + assert.NotNil(t, err) + assert.Equal(t, false, data) + + localDevice, remoteDevice, entites, _ := setupDevices(eebusService) + emobilty.evseEntity = entites[0] + emobilty.evEntity = entites[1] + + data, err = emobilty.EVOptimizationOfSelfConsumptionSupported() + assert.NotNil(t, err) + assert.Equal(t, false, data) + + emobilty.evLoadControl = loadcontrol(localDevice, emobilty.evEntity) + + data, err = emobilty.EVOptimizationOfSelfConsumptionSupported() + assert.Nil(t, err) + assert.Equal(t, false, data) + + datagram := datagramForEntityAndFeatures(true, localDevice, nil, model.FeatureTypeTypeNodeManagement, model.RoleTypeSpecial, model.RoleTypeSpecial) + + cmd := []model.CmdType{{ + NodeManagementUseCaseData: &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: util.Ptr(model.UseCaseActorTypeEV), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: util.Ptr(model.UseCaseNameTypeOptimizationOfSelfConsumptionDuringEVCharging), + UseCaseAvailable: util.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1, 2, 3}, + }, + }, + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVOptimizationOfSelfConsumptionSupported() + assert.NotNil(t, err) + assert.Equal(t, false, data) + + datagram = datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer, model.RoleTypeClient) + + cmd = []model.CmdType{{ + LoadControlLimitDescriptionListData: &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: util.Ptr(model.LoadControlLimitIdType(0)), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeRecommendation), + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVOptimizationOfSelfConsumptionSupported() + assert.Nil(t, err) + assert.Equal(t, true, data) +} diff --git a/emobility/public_EVPowerPerPhase_test.go b/emobility/public_EVPowerPerPhase_test.go new file mode 100644 index 0000000..ef1f7c8 --- /dev/null +++ b/emobility/public_EVPowerPerPhase_test.go @@ -0,0 +1,185 @@ +package emobility + +import ( + "testing" + + "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/eebus-go/util" + "github.com/stretchr/testify/assert" +) + +func Test_EVPowerPerPhase_Power(t *testing.T) { + emobilty, eebusService := setupEmobility() + + data, err := emobilty.EVPowerPerPhase() + assert.NotNil(t, err) + assert.Nil(t, data) + + localDevice, remoteDevice, entites, _ := setupDevices(eebusService) + emobilty.evseEntity = entites[0] + emobilty.evEntity = entites[1] + + data, err = emobilty.EVPowerPerPhase() + assert.NotNil(t, err) + assert.Nil(t, data) + + emobilty.evElectricalConnection = electricalConnection(localDevice, emobilty.evEntity) + emobilty.evMeasurement = measurement(localDevice, emobilty.evEntity) + + data, err = emobilty.EVPowerPerPhase() + assert.NotNil(t, err) + assert.Nil(t, data) + + datagram := datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer, model.RoleTypeClient) + + cmd := []model.CmdType{{ + ElectricalConnectionParameterDescriptionListData: &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeACPower), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + }, + }}} + + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVPowerPerPhase() + assert.NotNil(t, err) + assert.Nil(t, data) + + datagram = datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer, model.RoleTypeClient) + + cmd = []model.CmdType{{ + MeasurementDescriptionListData: &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypePower), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACPower), + }, + }, + }}} + + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVPowerPerPhase() + assert.NotNil(t, err) + assert.Nil(t, data) + + cmd = []model.CmdType{{ + MeasurementListData: &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(80), + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVPowerPerPhase() + assert.Nil(t, err) + assert.Equal(t, 80.0, data[0]) +} + +func Test_EVPowerPerPhase_Current(t *testing.T) { + emobilty, eebusService := setupEmobility() + + data, err := emobilty.EVPowerPerPhase() + assert.NotNil(t, err) + assert.Nil(t, data) + + localDevice, remoteDevice, entites, _ := setupDevices(eebusService) + emobilty.evseEntity = entites[0] + emobilty.evEntity = entites[1] + + data, err = emobilty.EVPowerPerPhase() + assert.NotNil(t, err) + assert.Nil(t, data) + + emobilty.evElectricalConnection = electricalConnection(localDevice, emobilty.evEntity) + emobilty.evMeasurement = measurement(localDevice, emobilty.evEntity) + + data, err = emobilty.EVPowerPerPhase() + assert.NotNil(t, err) + assert.Nil(t, data) + + datagram := datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer, model.RoleTypeClient) + + cmd := []model.CmdType{{ + ElectricalConnectionParameterDescriptionListData: &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeACCurrent), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + }, + }}} + + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVPowerPerPhase() + assert.NotNil(t, err) + assert.Nil(t, data) + + datagram = datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer, model.RoleTypeClient) + + cmd = []model.CmdType{{ + MeasurementDescriptionListData: &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeCurrent), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACCurrent), + }, + }, + }}} + + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVPowerPerPhase() + assert.NotNil(t, err) + assert.Nil(t, data) + + cmd = []model.CmdType{{ + MeasurementListData: &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVPowerPerPhase() + assert.Nil(t, err) + assert.Equal(t, 2300.0, data[0]) +} diff --git a/emobility/public_EVSoCSupported_test.go b/emobility/public_EVSoCSupported_test.go new file mode 100644 index 0000000..d4a1ad8 --- /dev/null +++ b/emobility/public_EVSoCSupported_test.go @@ -0,0 +1,77 @@ +package emobility + +import ( + "testing" + + "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/eebus-go/util" + "github.com/stretchr/testify/assert" +) + +func Test_EVSoCSupported(t *testing.T) { + emobilty, eebusService := setupEmobility() + + data, err := emobilty.EVSoCSupported() + assert.NotNil(t, err) + assert.Equal(t, false, data) + + localDevice, remoteDevice, entites, _ := setupDevices(eebusService) + emobilty.evseEntity = entites[0] + emobilty.evEntity = entites[1] + + data, err = emobilty.EVSoCSupported() + assert.NotNil(t, err) + assert.Equal(t, false, data) + + emobilty.evMeasurement = measurement(localDevice, emobilty.evEntity) + + data, err = emobilty.EVSoCSupported() + assert.Nil(t, err) + assert.Equal(t, false, data) + + datagram := datagramForEntityAndFeatures(true, localDevice, nil, model.FeatureTypeTypeNodeManagement, model.RoleTypeSpecial, model.RoleTypeSpecial) + + cmd := []model.CmdType{{ + NodeManagementUseCaseData: &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: util.Ptr(model.UseCaseActorTypeEV), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: util.Ptr(model.UseCaseNameTypeEVStateOfCharge), + UseCaseAvailable: util.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1}, + }, + }, + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVSoCSupported() + assert.NotNil(t, err) + assert.Equal(t, false, data) + + datagram = datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer, model.RoleTypeClient) + + cmd = []model.CmdType{{ + MeasurementDescriptionListData: &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeStateOfCharge), + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVSoCSupported() + assert.Nil(t, err) + assert.Equal(t, true, data) +} diff --git a/emobility/public_EVSoC_test.go b/emobility/public_EVSoC_test.go new file mode 100644 index 0000000..dee20db --- /dev/null +++ b/emobility/public_EVSoC_test.go @@ -0,0 +1,118 @@ +package emobility + +import ( + "testing" + + "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/eebus-go/util" + "github.com/stretchr/testify/assert" +) + +func Test_EVSoC(t *testing.T) { + emobilty, eebusService := setupEmobility() + + data, err := emobilty.EVSoC() + assert.NotNil(t, err) + assert.Equal(t, 0.0, data) + + localDevice, remoteDevice, entites, _ := setupDevices(eebusService) + emobilty.evseEntity = entites[0] + emobilty.evEntity = entites[1] + + data, err = emobilty.EVSoC() + assert.NotNil(t, err) + assert.Equal(t, 0.0, data) + + emobilty.evMeasurement = measurement(localDevice, emobilty.evEntity) + + data, err = emobilty.EVSoC() + assert.NotNil(t, err) + assert.Equal(t, 0.0, data) + + datagram := datagramForEntityAndFeatures(true, localDevice, nil, model.FeatureTypeTypeNodeManagement, model.RoleTypeSpecial, model.RoleTypeSpecial) + + cmd := []model.CmdType{{ + NodeManagementUseCaseData: &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: util.Ptr(model.UseCaseActorTypeEV), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: util.Ptr(model.UseCaseNameTypeEVStateOfCharge), + UseCaseAvailable: util.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1}, + }, + }, + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVSoC() + assert.NotNil(t, err) + assert.Equal(t, 0.0, data) + + datagram = datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer, model.RoleTypeClient) + + cmd = []model.CmdType{{ + MeasurementDescriptionListData: &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypePercentage), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeStateOfCharge), + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVSoC() + assert.NotNil(t, err) + assert.Equal(t, 0.0, data) + + datagram = datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer, model.RoleTypeClient) + + cmd = []model.CmdType{{ + MeasurementListData: &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVSoC() + assert.NotNil(t, err) + assert.Equal(t, 0.0, data) + + datagram = datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer, model.RoleTypeClient) + + cmd = []model.CmdType{{ + MeasurementListData: &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(80), + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + data, err = emobilty.EVSoC() + assert.Nil(t, err) + assert.Equal(t, 80.0, data) +} diff --git a/emobility/public_EVWriteIncentives_test.go b/emobility/public_EVWriteIncentives_test.go new file mode 100644 index 0000000..69d171e --- /dev/null +++ b/emobility/public_EVWriteIncentives_test.go @@ -0,0 +1,157 @@ +package emobility + +import ( + "encoding/json" + "testing" + "time" + + "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/eebus-go/util" + "github.com/stretchr/testify/assert" +) + +func Test_EVWriteIncentives(t *testing.T) { + emobilty, eebusService := setupEmobility() + + data := []EVDurationSlotValue{} + + err := emobilty.EVWriteIncentives(data) + assert.NotNil(t, err) + + localDevice, remoteDevice, entites, writeHandler := setupDevices(eebusService) + emobilty.evseEntity = entites[0] + emobilty.evEntity = entites[1] + + err = emobilty.EVWriteIncentives(data) + assert.NotNil(t, err) + + emobilty.evIncentiveTable = incentiveTableConfiguration(localDevice, emobilty.evEntity) + + err = emobilty.EVWriteIncentives(data) + assert.NotNil(t, err) + + datagram := datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeIncentiveTable, model.RoleTypeServer, model.RoleTypeClient) + + cmd := []model.CmdType{{ + IncentiveTableConstraintsData: &model.IncentiveTableConstraintsDataType{ + IncentiveTableConstraints: []model.IncentiveTableConstraintsType{ + { + IncentiveSlotConstraints: &model.TimeTableConstraintsDataType{ + SlotCountMin: util.Ptr(model.TimeSlotCountType(1)), + SlotCountMax: util.Ptr(model.TimeSlotCountType(10)), + }, + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + err = emobilty.EVWriteIncentives(data) + assert.NotNil(t, err) + + type dataStruct struct { + error bool + minSlots, maxSlots uint + slots []EVDurationSlotValue + } + + tests := []struct { + name string + data []dataStruct + }{ + { + "too few slots", + []dataStruct{ + { + true, 2, 2, + []EVDurationSlotValue{ + {Duration: time.Hour, Value: 0.1}, + }, + }, + }, + }, { + "too many slots", + []dataStruct{ + { + true, 1, 1, + []EVDurationSlotValue{ + {Duration: time.Hour, Value: 0.1}, + {Duration: time.Hour, Value: 0.1}, + }, + }, + }, + }, + { + "1 slot", + []dataStruct{ + { + false, 1, 1, + []EVDurationSlotValue{ + {Duration: time.Hour, Value: 0.1}, + }, + }, + }, + }, + { + "2 slots", + []dataStruct{ + { + false, 1, 2, + []EVDurationSlotValue{ + {Duration: time.Hour, Value: 0.1}, + {Duration: 30 * time.Minute, Value: 0.2}, + }, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + for _, data := range tc.data { + datagram = datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeIncentiveTable, model.RoleTypeServer, model.RoleTypeClient) + + cmd = []model.CmdType{{ + IncentiveTableConstraintsData: &model.IncentiveTableConstraintsDataType{ + IncentiveTableConstraints: []model.IncentiveTableConstraintsType{ + { + IncentiveSlotConstraints: &model.TimeTableConstraintsDataType{ + SlotCountMin: util.Ptr(model.TimeSlotCountType(data.minSlots)), + SlotCountMax: util.Ptr(model.TimeSlotCountType(data.maxSlots)), + }, + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + err = emobilty.EVWriteIncentives(data.slots) + if data.error { + assert.NotNil(t, err) + continue + } else { + assert.Nil(t, err) + } + + sentDatagram := model.Datagram{} + sentBytes := writeHandler.LastMessage() + err := json.Unmarshal(sentBytes, &sentDatagram) + assert.Nil(t, err) + + sentCmd := sentDatagram.Datagram.Payload.Cmd + assert.Equal(t, 1, len(sentCmd)) + + sentIncentiveData := sentCmd[0].IncentiveTableData.IncentiveTable[0].IncentiveSlot + assert.Equal(t, len(data.slots), len(sentIncentiveData)) + + for index, item := range sentIncentiveData { + assert.Equal(t, data.slots[index].Value, item.Tier[0].Incentive[0].Value.GetValue()) + } + } + }) + } +} diff --git a/emobility/public_EVWriteLoadControlLimits_test.go b/emobility/public_EVWriteLoadControlLimits_test.go new file mode 100644 index 0000000..e7dcdc1 --- /dev/null +++ b/emobility/public_EVWriteLoadControlLimits_test.go @@ -0,0 +1,255 @@ +package emobility + +import ( + "encoding/json" + "testing" + + "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/eebus-go/util" + "github.com/stretchr/testify/assert" + "golang.org/x/exp/slices" +) + +func Test_EVWriteLoadControlLimits(t *testing.T) { + emobilty, eebusService := setupEmobility() + + obligations := []float64{} + recommendations := []float64{} + + err := emobilty.EVWriteLoadControlLimits(obligations, recommendations) + assert.NotNil(t, err) + + localDevice, remoteDevice, entites, writeHandler := setupDevices(eebusService) + emobilty.evseEntity = entites[0] + emobilty.evEntity = entites[1] + + err = emobilty.EVWriteLoadControlLimits(obligations, recommendations) + assert.NotNil(t, err) + + emobilty.evElectricalConnection = electricalConnection(localDevice, emobilty.evEntity) + emobilty.evLoadControl = loadcontrol(localDevice, emobilty.evEntity) + + err = emobilty.EVWriteLoadControlLimits(obligations, recommendations) + assert.NotNil(t, err) + + datagram := datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer, model.RoleTypeClient) + + cmd := []model.CmdType{{ + ElectricalConnectionParameterDescriptionListData: &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(1)), + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(2)), + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeC), + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + err = emobilty.EVWriteLoadControlLimits(obligations, recommendations) + assert.NotNil(t, err) + + type dataStruct struct { + phases int + permittedDefaultExists bool + permittedDefaultValue float64 + permittedMinValue float64 + permittedMaxValue float64 + obligations, obligationsExpected []float64 + recommendations, recommendationsExpected []float64 + } + + tests := []struct { + name string + data []dataStruct + }{ + { + "1 Phase ISO15118", + []dataStruct{ + {1, true, 0.1, 2, 16, []float64{0}, []float64{0.1}, []float64{}, []float64{}}, + {1, true, 0.1, 2, 16, []float64{2.2}, []float64{2.2}, []float64{}, []float64{}}, + {1, true, 0.1, 2, 16, []float64{10}, []float64{10}, []float64{}, []float64{}}, + {1, true, 0.1, 2, 16, []float64{16}, []float64{16}, []float64{}, []float64{}}, + }, + }, + { + "3 Phase ISO15118", + []dataStruct{ + {3, true, 0.1, 2, 16, []float64{0, 0, 0}, []float64{0.1, 0.1, 0.1}, []float64{}, []float64{}}, + {3, true, 0.1, 2, 16, []float64{2.2, 2.2, 2.2}, []float64{2.2, 2.2, 2.2}, []float64{}, []float64{}}, + {3, true, 0.1, 2, 16, []float64{10, 10, 10}, []float64{10, 10, 10}, []float64{}, []float64{}}, + {3, true, 0.1, 2, 16, []float64{16, 16, 16}, []float64{16, 16, 16}, []float64{}, []float64{}}, + }, + }, + { + "1 Phase IEC61851", + []dataStruct{ + {1, true, 0, 6, 16, []float64{0}, []float64{0}, []float64{}, []float64{}}, + {1, true, 0, 6, 16, []float64{6}, []float64{6}, []float64{}, []float64{}}, + {1, true, 0, 6, 16, []float64{10}, []float64{10}, []float64{}, []float64{}}, + {1, true, 0, 6, 16, []float64{16}, []float64{16}, []float64{}, []float64{}}, + }, + }, + { + "3 Phase IEC61851", + []dataStruct{ + {3, true, 0, 6, 16, []float64{0, 0, 0}, []float64{0, 0, 0}, []float64{}, []float64{}}, + {3, true, 0, 6, 16, []float64{6, 6, 6}, []float64{6, 6, 6}, []float64{}, []float64{}}, + {3, true, 0, 6, 16, []float64{10, 10, 10}, []float64{10, 10, 10}, []float64{}, []float64{}}, + {3, true, 0, 6, 16, []float64{16, 16, 16}, []float64{16, 16, 16}, []float64{}, []float64{}}, + }, + }, + { + "3 Phase IEC61851 Elli", + []dataStruct{ + {3, false, 0, 6, 16, []float64{0, 0, 0}, []float64{0, 0, 0}, []float64{}, []float64{}}, + {3, false, 0, 6, 16, []float64{6, 6, 6}, []float64{6, 6, 6}, []float64{}, []float64{}}, + {3, false, 0, 6, 16, []float64{10, 10, 10}, []float64{10, 10, 10}, []float64{}, []float64{}}, + {3, false, 0, 6, 16, []float64{16, 16, 16}, []float64{16, 16, 16}, []float64{}, []float64{}}, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + dataSet := []model.ElectricalConnectionPermittedValueSetDataType{} + permittedData := []model.ScaledNumberSetType{} + for _, data := range tc.data { + for phase := 0; phase < data.phases; phase++ { + item := model.ScaledNumberSetType{ + Range: []model.ScaledNumberRangeType{ + { + Min: model.NewScaledNumberType(data.permittedMinValue), + Max: model.NewScaledNumberType(data.permittedMaxValue), + }, + }, + } + if data.permittedDefaultExists { + item.Value = []model.ScaledNumberType{*model.NewScaledNumberType(data.permittedDefaultValue)} + } + permittedData = append(permittedData, item) + + permittedItem := model.ElectricalConnectionPermittedValueSetDataType{ + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(phase)), + PermittedValueSet: permittedData, + } + dataSet = append(dataSet, permittedItem) + } + + datagram = datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer, model.RoleTypeClient) + + cmd = []model.CmdType{{ + ElectricalConnectionPermittedValueSetListData: &model.ElectricalConnectionPermittedValueSetListDataType{ + ElectricalConnectionPermittedValueSetData: dataSet, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + err = emobilty.EVWriteLoadControlLimits(obligations, recommendations) + assert.NotNil(t, err) + + datagram = datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer, model.RoleTypeClient) + + limitDesc := []model.LoadControlLimitDescriptionDataType{} + var limitIdsObligation, limitIdsRecommendation []model.LoadControlLimitIdType + for index := range data.obligations { + id := model.LoadControlLimitIdType(index) + limitItem := model.LoadControlLimitDescriptionDataType{ + LimitId: util.Ptr(id), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + MeasurementId: util.Ptr(model.MeasurementIdType(index)), + } + limitDesc = append(limitDesc, limitItem) + limitIdsObligation = append(limitIdsObligation, id) + } + add := len(limitDesc) + for index := range data.recommendations { + id := model.LoadControlLimitIdType(index + add) + limitItem := model.LoadControlLimitDescriptionDataType{ + LimitId: util.Ptr(id), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeRecommendation), + MeasurementId: util.Ptr(model.MeasurementIdType(index + add)), + } + limitDesc = append(limitDesc, limitItem) + limitIdsRecommendation = append(limitIdsRecommendation, id) + } + + cmd = []model.CmdType{{ + LoadControlLimitDescriptionListData: &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: limitDesc, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + err = emobilty.EVWriteLoadControlLimits(obligations, recommendations) + assert.NotNil(t, err) + + limitData := []model.LoadControlLimitDataType{} + for index := range limitDesc { + limitItem := model.LoadControlLimitDataType{ + LimitId: util.Ptr(model.LoadControlLimitIdType(index)), + IsLimitChangeable: util.Ptr(true), + } + limitData = append(limitData, limitItem) + } + sentLimits := len(limitData) + + cmd = []model.CmdType{{ + LoadControlLimitListData: &model.LoadControlLimitListDataType{ + LoadControlLimitData: limitData, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + err = emobilty.EVWriteLoadControlLimits(obligations, recommendations) + assert.NotNil(t, err) + + err = emobilty.EVWriteLoadControlLimits(data.obligations, data.recommendations) + assert.Nil(t, err) + + sentDatagram := model.Datagram{} + sentBytes := writeHandler.LastMessage() + err := json.Unmarshal(sentBytes, &sentDatagram) + assert.Nil(t, err) + + sentCmd := sentDatagram.Datagram.Payload.Cmd + assert.Equal(t, 1, len(sentCmd)) + + sentLimitData := sentCmd[0].LoadControlLimitListData.LoadControlLimitData + assert.Equal(t, sentLimits, len(sentLimitData)) + + for _, item := range sentLimitData { + if index := slices.Index(limitIdsObligation, *item.LimitId); index >= 0 { + assert.Equal(t, data.obligationsExpected[index], item.Value.GetValue()) + } + if index := slices.Index(limitIdsRecommendation, *item.LimitId); index >= 0 { + assert.Equal(t, data.recommendationsExpected[index], item.Value.GetValue()) + } + } + } + }) + } +} diff --git a/emobility/public_EVWritePowerLimits_test.go b/emobility/public_EVWritePowerLimits_test.go new file mode 100644 index 0000000..06a8c30 --- /dev/null +++ b/emobility/public_EVWritePowerLimits_test.go @@ -0,0 +1,154 @@ +package emobility + +import ( + "encoding/json" + "testing" + "time" + + "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/eebus-go/util" + "github.com/stretchr/testify/assert" +) + +func Test_EVWritePowerLimits(t *testing.T) { + emobilty, eebusService := setupEmobility() + + data := []EVDurationSlotValue{} + + err := emobilty.EVWritePowerLimits(data) + assert.NotNil(t, err) + + localDevice, remoteDevice, entites, writeHandler := setupDevices(eebusService) + emobilty.evseEntity = entites[0] + emobilty.evEntity = entites[1] + + err = emobilty.EVWritePowerLimits(data) + assert.NotNil(t, err) + + emobilty.evTimeSeries = timeSeriesConfiguration(localDevice, emobilty.evEntity) + + err = emobilty.EVWritePowerLimits(data) + assert.NotNil(t, err) + + datagram := datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeServer, model.RoleTypeClient) + + cmd := []model.CmdType{{ + TimeSeriesDescriptionListData: &model.TimeSeriesDescriptionListDataType{ + TimeSeriesDescriptionData: []model.TimeSeriesDescriptionDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeConstraints), + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + err = emobilty.EVWritePowerLimits(data) + assert.NotNil(t, err) + + type dataStruct struct { + error bool + minSlots, maxSlots uint + slots []EVDurationSlotValue + } + + tests := []struct { + name string + data []dataStruct + }{ + { + "too few slots", + []dataStruct{ + { + true, 2, 2, + []EVDurationSlotValue{ + {Duration: time.Hour, Value: 11000}, + }, + }, + }, + }, { + "too many slots", + []dataStruct{ + { + true, 1, 1, + []EVDurationSlotValue{ + {Duration: time.Hour, Value: 11000}, + {Duration: time.Hour, Value: 11000}, + }, + }, + }, + }, + { + "1 slot", + []dataStruct{ + { + false, 1, 1, + []EVDurationSlotValue{ + {Duration: time.Hour, Value: 11000}, + }, + }, + }, + }, + { + "2 slots", + []dataStruct{ + { + false, 1, 2, + []EVDurationSlotValue{ + {Duration: time.Hour, Value: 11000}, + {Duration: 30 * time.Minute, Value: 5000}, + }, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + for _, data := range tc.data { + datagram = datagramForEntityAndFeatures(false, localDevice, emobilty.evEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeServer, model.RoleTypeClient) + + cmd = []model.CmdType{{ + TimeSeriesConstraintsListData: &model.TimeSeriesConstraintsListDataType{ + TimeSeriesConstraintsData: []model.TimeSeriesConstraintsDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + SlotCountMin: util.Ptr(model.TimeSeriesSlotCountType(data.minSlots)), + SlotCountMax: util.Ptr(model.TimeSeriesSlotCountType(data.maxSlots)), + }, + }, + }}} + datagram.Payload.Cmd = cmd + + err = localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) + + err = emobilty.EVWritePowerLimits(data.slots) + if data.error { + assert.NotNil(t, err) + continue + } else { + assert.Nil(t, err) + } + + sentDatagram := model.Datagram{} + sentBytes := writeHandler.LastMessage() + err := json.Unmarshal(sentBytes, &sentDatagram) + assert.Nil(t, err) + + sentCmd := sentDatagram.Datagram.Payload.Cmd + assert.Equal(t, 1, len(sentCmd)) + + sentPowerLimitsData := sentCmd[0].TimeSeriesListData.TimeSeriesData[0].TimeSeriesSlot + assert.Equal(t, len(data.slots), len(sentPowerLimitsData)) + + for index, item := range sentPowerLimitsData { + assert.Equal(t, data.slots[index].Value, item.MaxValue.GetValue()) + } + } + }) + } +} diff --git a/emobility/scenario.go b/emobility/scenario.go index ea3d1fb..73d09ad 100644 --- a/emobility/scenario.go +++ b/emobility/scenario.go @@ -7,6 +7,7 @@ import ( "github.com/enbility/eebus-go/service" "github.com/enbility/eebus-go/spine" "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/eebus-go/util" ) type EmobilityScenarioImpl struct { @@ -15,14 +16,19 @@ type EmobilityScenarioImpl struct { remoteDevices map[string]*EMobilityImpl mux sync.Mutex + + currency model.CurrencyType + configuration EmobilityConfiguration } var _ scenarios.ScenariosI = (*EmobilityScenarioImpl)(nil) -func NewEMobilityScenario(service *service.EEBUSService) *EmobilityScenarioImpl { +func NewEMobilityScenario(service *service.EEBUSService, currency model.CurrencyType, configuration EmobilityConfiguration) *EmobilityScenarioImpl { return &EmobilityScenarioImpl{ ScenarioImpl: scenarios.NewScenarioImpl(service), remoteDevices: make(map[string]*EMobilityImpl), + currency: currency, + configuration: configuration, } } @@ -30,46 +36,38 @@ func NewEMobilityScenario(service *service.EEBUSService) *EmobilityScenarioImpl func (e *EmobilityScenarioImpl) AddFeatures() { localEntity := e.Service.LocalEntity() + // server features { - f := localEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeClient, "Device Configuration Client") - f.AddResultHandler(e) - } - { - f := localEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceClassification, model.RoleTypeClient, "Device Classification Client") - f.AddResultHandler(e) - } - { - f := localEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeClient, "Device Diagnosis Client") - f.AddResultHandler(e) - } - { - f := localEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer, "Device Diagnosis Server") + f := localEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) f.AddResultHandler(e) f.AddFunctionType(model.FunctionTypeDeviceDiagnosisStateData, true, false) // Set the initial state - state := model.DeviceDiagnosisOperatingStateTypeNormalOperation deviceDiagnosisStateDate := &model.DeviceDiagnosisStateDataType{ - OperatingState: &state, + OperatingState: util.Ptr(model.DeviceDiagnosisOperatingStateTypeNormalOperation), } f.SetData(model.FunctionTypeDeviceDiagnosisStateData, deviceDiagnosisStateDate) f.AddFunctionType(model.FunctionTypeDeviceDiagnosisHeartbeatData, true, false) } - { - f := localEntity.GetOrAddFeature(model.FeatureTypeTypeElectricalConnection, model.RoleTypeClient, "Electrical Connection Client") - f.AddResultHandler(e) - } - { - f := localEntity.GetOrAddFeature(model.FeatureTypeTypeMeasurement, model.RoleTypeClient, "Measurement Client") - f.AddResultHandler(e) + + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceDiagnosis, + model.FeatureTypeTypeDeviceClassification, + model.FeatureTypeTypeDeviceConfiguration, + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + model.FeatureTypeTypeLoadControl, + model.FeatureTypeTypeIdentification, } - { - f := localEntity.GetOrAddFeature(model.FeatureTypeTypeIdentification, model.RoleTypeClient, "Identification Client") - f.AddResultHandler(e) + + if e.configuration.CoordinatedChargingEnabled { + clientFeatures = append(clientFeatures, model.FeatureTypeTypeTimeSeries) + clientFeatures = append(clientFeatures, model.FeatureTypeTypeIncentiveTable) } - { - f := localEntity.GetOrAddFeature(model.FeatureTypeTypeLoadControl, model.RoleTypeClient, "LoadControl Client") + for _, feature := range clientFeatures { + f := localEntity.GetOrAddFeature(feature, model.RoleTypeClient) f.AddResultHandler(e) } } @@ -114,14 +112,16 @@ func (e *EmobilityScenarioImpl) AddUseCases() { model.SpecificationVersionType("1.0.1b"), []model.UseCaseScenarioSupportType{1, 2, 3}) - _ = spine.NewUseCase( - localEntity, - model.UseCaseNameTypeCoordinatedEVCharging, - model.SpecificationVersionType("1.0.1"), - []model.UseCaseScenarioSupportType{1, 2, 3, 4, 5, 6, 7, 8}) + if e.configuration.CoordinatedChargingEnabled { + _ = spine.NewUseCase( + localEntity, + model.UseCaseNameTypeCoordinatedEVCharging, + model.SpecificationVersionType("1.0.1"), + []model.UseCaseScenarioSupportType{1, 2, 3, 4, 5, 6, 7, 8}) + } } -func (e *EmobilityScenarioImpl) RegisterEmobilityRemoteDevice(details *service.ServiceDetails) *EMobilityImpl { +func (e *EmobilityScenarioImpl) RegisterRemoteDevice(details *service.ServiceDetails, dataProvider any) any { // TODO: emobility should be stored per remote SKI and // only be set for the SKI if the device supports it e.mux.Lock() @@ -131,12 +131,16 @@ func (e *EmobilityScenarioImpl) RegisterEmobilityRemoteDevice(details *service.S return em } - emobility := NewEMobility(e.Service, details) + var provider EmobilityDataProvider + if dataProvider != nil { + provider = dataProvider.(EmobilityDataProvider) + } + emobility := NewEMobility(e.Service, details, e.currency, e.configuration, provider) e.remoteDevices[details.SKI()] = emobility return emobility } -func (e *EmobilityScenarioImpl) UnRegisterEmobilityRemoteDevice(remoteDeviceSki string) error { +func (e *EmobilityScenarioImpl) UnRegisterRemoteDevice(remoteDeviceSki string) error { e.mux.Lock() defer e.mux.Unlock() diff --git a/emobility/types.go b/emobility/types.go index cf63178..40bd877 100644 --- a/emobility/types.go +++ b/emobility/types.go @@ -1,8 +1,13 @@ package emobility -import "errors" +import ( + "errors" + "time" -type EVCommunicationStandardType string + "github.com/enbility/eebus-go/spine/model" +) + +type EVCommunicationStandardType model.DeviceConfigurationKeyValueStringType const ( EVCommunicationStandardTypeUnknown EVCommunicationStandardType = "unknown" @@ -28,7 +33,49 @@ const ( EVChargeStrategyTypeUnknown EVChargeStrategyType = "unknown" EVChargeStrategyTypeNoDemand EVChargeStrategyType = "nodemand" EVChargeStrategyTypeDirectCharging EVChargeStrategyType = "directcharging" + EVChargeStrategyTypeMinSoC EVChargeStrategyType = "minsoc" EVChargeStrategyTypeTimedCharging EVChargeStrategyType = "timedcharging" ) +// Contains details about the actual demands from the EV +// +// General: +// - If duration and energy is 0, charge mode is EVChargeStrategyTypeNoDemand +// - If duration is 0, charge mode is EVChargeStrategyTypeDirectCharging and the slots should cover at least 48h +// - If both are != 0, charge mode is EVChargeStrategyTypeTimedCharging and the slots should cover at least the duration, but at max 168h (7d) +type EVDemand struct { + MinDemand float64 // minimum demand in Wh to reach the minSoC setting, 0 if not set + OptDemand float64 // demand in Wh to reach the timer SoC setting + MaxDemand float64 // the maximum possible demand until the battery is full + DurationUntilStart time.Duration // the duration from now until charging will start, this could be in the future but usualy is now + DurationUntilEnd time.Duration // the duration from now until minDemand or optDemand has to be reached, 0 if direct charge strategy is active +} + +// Details about the time slot constraints +type EVTimeSlotConstraints struct { + MinSlots uint // the minimum number of slots, no minimum if 0 + MaxSlots uint // the maximum number of slots, unlimited if 0 + MinSlotDuration time.Duration // the minimum duration of a slot, no minimum if 0 + MaxSlotDuration time.Duration // the maximum duration of a slot, unlimited if 0 + SlotDurationStepSize time.Duration // the duration has to be a multiple of this value if != 0 +} + +// Details about the incentive slot constraints +type EVIncentiveSlotConstraints struct { + MinSlots uint // the minimum number of slots, no minimum if 0 + MaxSlots uint // the maximum number of slots, unlimited if 0 +} + +// Contains details about power limits or incentives for a defined timeframe +type EVDurationSlotValue struct { + Duration time.Duration // Duration of this slot + Value float64 // Energy Cost or Power Limit +} + var ErrEVDisconnected = errors.New("ev is disconnected") +var ErrNotSupported = errors.New("function is not supported") + +// Allows to exclude some features +type EmobilityConfiguration struct { + CoordinatedChargingEnabled bool +} diff --git a/go.mod b/go.mod index 712b1f6..138a14a 100644 --- a/go.mod +++ b/go.mod @@ -2,15 +2,22 @@ module github.com/enbility/cemd go 1.18 -require github.com/enbility/eebus-go v0.1.7 +require ( + github.com/enbility/eebus-go v0.2.0 + github.com/golang/mock v1.6.0 + github.com/stretchr/testify v1.8.2 + golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15 +) require ( github.com/ahmetb/go-linq/v3 v3.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/holoplot/go-avahi v1.0.1 // indirect github.com/libp2p/zeroconf/v2 v2.2.0 // indirect github.com/miekg/dns v1.1.52 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rickb777/date v1.20.1 // indirect github.com/rickb777/plural v1.4.1 // indirect gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect @@ -18,4 +25,5 @@ require ( golang.org/x/net v0.8.0 // indirect golang.org/x/sys v0.6.0 // indirect golang.org/x/tools v0.7.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 1188807..8006c4c 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,15 @@ github.com/ahmetb/go-linq/v3 v3.2.0 h1:BEuMfp+b59io8g5wYzNoFe9pWPalRklhlhbiU3hYZDE= github.com/ahmetb/go-linq/v3 v3.2.0/go.mod h1:haQ3JfOeWK8HpVxMtHHEMPVgBKiYyQ+f1/kLZh/cj9U= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/enbility/eebus-go v0.1.7 h1:iPOp3oJ12cExOfS9tcBbsHRSOyr+uKGypjAjRP8SdaE= -github.com/enbility/eebus-go v0.1.7/go.mod h1:Ozg1eDUfSbHfQ1dWfyAUa3h8dMtgM/01eO30kHca5zk= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/enbility/eebus-go v0.2.0 h1:znQUfG1QYk0Q+vOacrsSNtXmitF1F2Rx9+ohwcRNlRw= +github.com/enbility/eebus-go v0.2.0/go.mod h1:Ozg1eDUfSbHfQ1dWfyAUa3h8dMtgM/01eO30kHca5zk= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -18,33 +22,64 @@ github.com/miekg/dns v1.1.52 h1:Bmlc/qsNNULOe6bpXcUTsuOajd0DzRHwup6D9k1An0c= github.com/miekg/dns v1.1.52/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= github.com/onsi/gomega v1.24.0 h1:+0glovB9Jd6z3VR+ScSwQqXVTIfJcGA9UBM8yzQxhqg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rickb777/date v1.20.1 h1:7MzSOc42Hbr5UXiQOihAAXoYDoeyzr0Hwvt+hCjBDV4= github.com/rickb777/date v1.20.1/go.mod h1:9MqjVxT6a/AQTA4nxj9E6G3ksQiMESTn9/9kfE+CvwU= github.com/rickb777/plural v1.4.1 h1:5MMLcbIaapLFmvDGRT5iPk8877hpTPt8Y9cdSKRw9sU= github.com/rickb777/plural v1.4.1/go.mod h1:kdmXUpmKBJTS0FtG/TFumd//VBWsNTD7zOw7x4umxNw= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a h1:DxppxFKRqJ8WD6oJ3+ZXKDY0iMONQDl5UTg2aTyHh8k= gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a/go.mod h1:NREvu3a57BaK0R1+ztrEzHWiZAihohNLQ6trPxlIqZI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15 h1:5oN1Pz/eDhCpbMbLstvIPa0b/BEQo6g6nwV3pLjfM6w= +golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/grid/events.go b/grid/events.go new file mode 100644 index 0000000..e263de6 --- /dev/null +++ b/grid/events.go @@ -0,0 +1,139 @@ +package grid + +import ( + "github.com/enbility/eebus-go/features" + "github.com/enbility/eebus-go/logging" + "github.com/enbility/eebus-go/spine" + "github.com/enbility/eebus-go/spine/model" +) + +// Internal EventHandler Interface for the CEM +func (e *GridImpl) HandleEvent(payload spine.EventPayload) { + // we only care about the registered SKI + if payload.Ski != e.ski { + return + } + + // we care only about events for this remote device + if payload.Device != nil && payload.Device.Ski() != e.ski { + return + } + + switch payload.EventType { + case spine.EventTypeDeviceChange: + switch payload.ChangeType { + case spine.ElementChangeRemove: + e.gridDisconnected() + } + + case spine.EventTypeEntityChange: + entityType := payload.Entity.EntityType() + + switch payload.ChangeType { + case spine.ElementChangeAdd: + switch entityType { + case model.EntityTypeTypeGridConnectionPointOfPremises: + e.gridConnected(payload.Ski, payload.Entity) + } + case spine.ElementChangeRemove: + switch entityType { + case model.EntityTypeTypeGridConnectionPointOfPremises: + e.gridDisconnected() + } + } + + case spine.EventTypeDataChange: + if payload.ChangeType == spine.ElementChangeUpdate { + switch payload.Data.(type) { + + case *model.DeviceConfigurationKeyValueDescriptionListDataType: + // key value descriptions received, now get the data + if _, err := e.gridDeviceConfiguration.RequestKeyValues(); err != nil { + logging.Log.Error("Error getting configuration key values:", err) + } + + case *model.MeasurementDescriptionListDataType: + if _, err := e.gridMeasurement.RequestValues(); err != nil { + logging.Log.Error("Error getting measurement list values:", err) + } + } + + } + + } +} + +// process required steps when a grid device is connected +func (e *GridImpl) gridConnected(ski string, entity *spine.EntityRemoteImpl) { + e.gridEntity = entity + localDevice := e.service.LocalDevice() + + f1, err := features.NewDeviceConfiguration(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) + if err != nil { + return + } + e.gridDeviceConfiguration = f1 + + f2, err := features.NewElectricalConnection(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) + if err != nil { + return + } + e.gridElectricalConnection = f2 + + f3, err := features.NewMeasurement(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) + if err != nil { + return + } + e.gridMeasurement = f3 + + // subscribe + if err := e.gridDeviceConfiguration.SubscribeForEntity(); err != nil { + logging.Log.Error(err) + return + } + if err := e.gridElectricalConnection.SubscribeForEntity(); err != nil { + logging.Log.Error(err) + return + } + if err := e.gridMeasurement.SubscribeForEntity(); err != nil { + logging.Log.Error(err) + return + } + + // get configuration data + if err := e.gridDeviceConfiguration.RequestDescriptions(); err != nil { + logging.Log.Error(err) + return + } + + // get electrical connection parameter + if err := e.gridElectricalConnection.RequestDescriptions(); err != nil { + logging.Log.Error(err) + return + } + + if err := e.gridElectricalConnection.RequestParameterDescriptions(); err != nil { + logging.Log.Error(err) + return + } + + // get measurement parameters + if err := e.gridMeasurement.RequestDescriptions(); err != nil { + logging.Log.Error(err) + return + } + + if err := e.gridMeasurement.RequestConstraints(); err != nil { + logging.Log.Error(err) + return + } +} + +// a grid device was disconnected +func (e *GridImpl) gridDisconnected() { + e.gridEntity = nil + + e.gridDeviceConfiguration = nil + e.gridElectricalConnection = nil + e.gridMeasurement = nil +} diff --git a/grid/grid.go b/grid/grid.go new file mode 100644 index 0000000..4b6d985 --- /dev/null +++ b/grid/grid.go @@ -0,0 +1,50 @@ +package grid + +import ( + "github.com/enbility/eebus-go/features" + "github.com/enbility/eebus-go/service" + "github.com/enbility/eebus-go/spine" + "github.com/enbility/eebus-go/util" +) + +type GridI interface { + PowerLimitationFactor() (float64, error) + MomentaryPowerConsumptionOrProduction() (float64, error) + TotalFeedInEnergy() (float64, error) + TotalConsumedEnergy() (float64, error) + MomentaryCurrentConsumptionOrProduction() ([]float64, error) + Voltage() ([]float64, error) + Frequency() (float64, error) +} + +type GridImpl struct { + entity *spine.EntityLocalImpl + + service *service.EEBUSService + + gridEntity *spine.EntityRemoteImpl + + gridDeviceConfiguration *features.DeviceConfiguration + gridElectricalConnection *features.ElectricalConnection + gridMeasurement *features.Measurement + + ski string +} + +var _ GridI = (*GridImpl)(nil) + +// Add Grid support +func NewGrid(service *service.EEBUSService, details *service.ServiceDetails) *GridImpl { + ski := util.NormalizeSKI(details.SKI()) + + grid := &GridImpl{ + service: service, + entity: service.LocalEntity(), + ski: ski, + } + spine.Events.Subscribe(grid) + + service.PairRemoteService(details) + + return grid +} diff --git a/grid/public.go b/grid/public.go new file mode 100644 index 0000000..31d9124 --- /dev/null +++ b/grid/public.go @@ -0,0 +1,255 @@ +package grid + +import ( + "github.com/enbility/cemd/util" + "github.com/enbility/eebus-go/features" + "github.com/enbility/eebus-go/spine/model" +) + +// return the power limitation factor +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - ErrNotSupported if getting the communication standard is not supported +// - and others +func (g *GridImpl) PowerLimitationFactor() (float64, error) { + if g.gridEntity == nil { + return 0, util.ErrDeviceDisconnected + } + + if g.gridMeasurement == nil { + return 0, features.ErrDataNotAvailable + } + + keyname := model.DeviceConfigurationKeyNameTypePvCurtailmentLimitFactor + + // check if device configuration description has curtailment limit factor key name + _, err := g.gridDeviceConfiguration.GetDescriptionForKeyName(keyname) + if err != nil { + return 0, err + } + + data, err := g.gridDeviceConfiguration.GetKeyValueForKeyName(keyname, model.DeviceConfigurationKeyValueTypeTypeScaledNumber) + if err != nil { + return 0, err + } + + if data == nil { + return 0, features.ErrDataNotAvailable + } + + value := data.(*model.ScaledNumberType) + return value.GetValue(), nil +} + +// return the momentary power consumption (positive) or production (negative) +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (g *GridImpl) MomentaryPowerConsumptionOrProduction() (float64, error) { + measurement := model.MeasurementTypeTypePower + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeACPowerTotal + data, err := g.getValuesForTypeCommodityScope(measurement, commodity, scope) + if err != nil { + return 0, err + } + + // we assume there is only one value + mId := data[0].MeasurementId + value := data[0].Value + if mId == nil || value == nil { + return 0, features.ErrDataNotAvailable + } + + // according to UC_TS_MonitoringOfGridConnectionPoint 3.2.2.2.4.1 + // positive values are with description "PositiveEnergyDirection" value "consume" + // but we verify this + desc, err := g.gridElectricalConnection.GetDescriptionForMeasurementId(*mId) + if err != nil { + return 0, err + } + + // if energy direction is not consume, invert it + if desc.PositiveEnergyDirection != nil && *desc.PositiveEnergyDirection != model.EnergyDirectionTypeConsume { + return -1 * value.GetValue(), nil + } + + return value.GetValue(), nil +} + +// return the total feed-in energy +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (g *GridImpl) TotalFeedInEnergy() (float64, error) { + measurement := model.MeasurementTypeTypeEnergy + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeGridFeedIn + data, err := g.getValuesForTypeCommodityScope(measurement, commodity, scope) + if err != nil { + return 0, err + } + + // we assume thre is only one result + value := data[0].Value + if value == nil { + return 0, features.ErrDataNotAvailable + } + + return value.GetValue(), nil +} + +// return the total consumed energy +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (g *GridImpl) TotalConsumedEnergy() (float64, error) { + measurement := model.MeasurementTypeTypeEnergy + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeGridConsumption + data, err := g.getValuesForTypeCommodityScope(measurement, commodity, scope) + if err != nil { + return 0, err + } + + // we assume thre is only one result + value := data[0].Value + if value == nil { + return 0, features.ErrDataNotAvailable + } + + return value.GetValue(), nil +} + +// return the momentary current consumption (positive) or production (negative) per phase +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (g *GridImpl) MomentaryCurrentConsumptionOrProduction() ([]float64, error) { + measurement := model.MeasurementTypeTypeCurrent + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeACCurrent + values, err := g.getValuesForTypeCommodityScope(measurement, commodity, scope) + if err != nil { + return nil, err + } + + var phaseA, phaseB, phaseC float64 + + for _, item := range values { + if item.Value == nil || item.MeasurementId == nil { + continue + } + + param, err := g.gridElectricalConnection.GetParameterDescriptionForMeasurementId(*item.MeasurementId) + if err != nil || param.AcMeasuredPhases == nil { + continue + } + + value := item.Value.GetValue() + + // according to UC_TS_MonitoringOfGridConnectionPoint 3.2.1.3.2.4 + // positive values are with description "PositiveEnergyDirection" value "consume" + // but we should verify this + if desc, err := g.gridElectricalConnection.GetDescriptionForMeasurementId(*item.MeasurementId); err == nil { + // if energy direction is not consume, invert it + if desc.PositiveEnergyDirection != nil && *desc.PositiveEnergyDirection != model.EnergyDirectionTypeConsume { + value = -1 * value + } + } + + switch *param.AcMeasuredPhases { + case model.ElectricalConnectionPhaseNameTypeA: + phaseA = value + case model.ElectricalConnectionPhaseNameTypeB: + phaseB = value + case model.ElectricalConnectionPhaseNameTypeC: + phaseC = value + } + } + + return []float64{phaseA, phaseB, phaseC}, nil +} + +// return the voltage per phase +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (g *GridImpl) Voltage() ([]float64, error) { + measurement := model.MeasurementTypeTypeVoltage + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeACVoltage + data, err := g.getValuesForTypeCommodityScope(measurement, commodity, scope) + if err != nil { + return nil, err + } + + var phaseA, phaseB, phaseC float64 + + for _, item := range data { + if item.Value == nil || item.MeasurementId == nil { + continue + } + + param, err := g.gridElectricalConnection.GetParameterDescriptionForMeasurementId(*item.MeasurementId) + if err != nil || param.AcMeasuredPhases == nil { + continue + } + + value := item.Value.GetValue() + + switch *param.AcMeasuredPhases { + case model.ElectricalConnectionPhaseNameTypeA: + phaseA = value + case model.ElectricalConnectionPhaseNameTypeB: + phaseB = value + case model.ElectricalConnectionPhaseNameTypeC: + phaseC = value + } + } + + return []float64{phaseA, phaseB, phaseC}, nil +} + +// return the frequence +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (g *GridImpl) Frequency() (float64, error) { + measurement := model.MeasurementTypeTypeFrequency + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeACFrequency + item, err := g.getValuesForTypeCommodityScope(measurement, commodity, scope) + if err != nil { + return 0, err + } + + // take the first item + value := item[0].Value + if value == nil { + return 0, features.ErrDataNotAvailable + } + + return value.GetValue(), nil +} + +// helper + +func (g *GridImpl) getValuesForTypeCommodityScope(measurement model.MeasurementTypeType, commodity model.CommodityTypeType, scope model.ScopeTypeType) ([]model.MeasurementDataType, error) { + if g.gridEntity == nil { + return nil, util.ErrDeviceDisconnected + } + + if g.gridMeasurement == nil { + return nil, features.ErrDataNotAvailable + } + + return g.gridMeasurement.GetValuesForTypeCommodityScope(measurement, commodity, scope) +} diff --git a/grid/results.go b/grid/results.go new file mode 100644 index 0000000..0d6a6fa --- /dev/null +++ b/grid/results.go @@ -0,0 +1,8 @@ +package grid + +import ( + "github.com/enbility/eebus-go/spine" +) + +func (e *GridImpl) HandleResult(errorMsg spine.ResultMessage) { +} diff --git a/grid/scenario.go b/grid/scenario.go new file mode 100644 index 0000000..8e1700d --- /dev/null +++ b/grid/scenario.go @@ -0,0 +1,94 @@ +package grid + +import ( + "sync" + + "github.com/enbility/cemd/scenarios" + "github.com/enbility/eebus-go/service" + "github.com/enbility/eebus-go/spine" + "github.com/enbility/eebus-go/spine/model" +) + +type GridScenarioImpl struct { + *scenarios.ScenarioImpl + + remoteDevices map[string]*GridImpl + + mux sync.Mutex +} + +var _ scenarios.ScenariosI = (*GridScenarioImpl)(nil) + +func NewGridScenario(service *service.EEBUSService) *GridScenarioImpl { + return &GridScenarioImpl{ + ScenarioImpl: scenarios.NewScenarioImpl(service), + remoteDevices: make(map[string]*GridImpl), + } +} + +// adds all the supported features to the local entity +func (e *GridScenarioImpl) AddFeatures() { + localEntity := e.Service.LocalEntity() + + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceConfiguration, + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + } + for _, feature := range clientFeatures { + f := localEntity.GetOrAddFeature(feature, model.RoleTypeClient) + f.AddResultHandler(e) + } +} + +// add supported grid usecases +func (e *GridScenarioImpl) AddUseCases() { + localEntity := e.Service.LocalEntity() + + _ = spine.NewUseCase( + localEntity, + model.UseCaseNameTypeMonitoringOfGridConnectionPoint, + model.SpecificationVersionType("1.0.0 RC5"), + []model.UseCaseScenarioSupportType{1, 2, 3, 4, 5, 6, 7}) +} + +func (e *GridScenarioImpl) RegisterRemoteDevice(details *service.ServiceDetails, dataProvider any) any { + // TODO: grid should be stored per remote SKI and + // only be set for the SKI if the device supports it + e.mux.Lock() + defer e.mux.Unlock() + + if em, ok := e.remoteDevices[details.SKI()]; ok { + return em + } + + grid := NewGrid(e.Service, details) + e.remoteDevices[details.SKI()] = grid + return grid +} + +func (e *GridScenarioImpl) UnRegisterRemoteDevice(remoteDeviceSki string) error { + e.mux.Lock() + defer e.mux.Unlock() + + delete(e.remoteDevices, remoteDeviceSki) + + return e.Service.UnpairRemoteService(remoteDeviceSki) +} + +func (e *GridScenarioImpl) HandleResult(errorMsg spine.ResultMessage) { + e.mux.Lock() + defer e.mux.Unlock() + + if errorMsg.DeviceRemote == nil { + return + } + + em, ok := e.remoteDevices[errorMsg.DeviceRemote.Ski()] + if !ok { + return + } + + em.HandleResult(errorMsg) +} diff --git a/inverterbatteryvis/events.go b/inverterbatteryvis/events.go new file mode 100644 index 0000000..e43a02e --- /dev/null +++ b/inverterbatteryvis/events.go @@ -0,0 +1,131 @@ +package inverterbatteryvis + +import ( + "github.com/enbility/eebus-go/features" + "github.com/enbility/eebus-go/logging" + "github.com/enbility/eebus-go/spine" + "github.com/enbility/eebus-go/spine/model" +) + +// Internal EventHandler Interface for the CEM +func (i *InverterBatteryVisImpl) HandleEvent(payload spine.EventPayload) { + // we only care about the registered SKI + if payload.Ski != i.ski { + return + } + + // we care only about events for this remote device + if payload.Device != nil && payload.Device.Ski() != i.ski { + return + } + + switch payload.EventType { + case spine.EventTypeDeviceChange: + switch payload.ChangeType { + case spine.ElementChangeRemove: + i.inverterDisconnected() + } + + case spine.EventTypeEntityChange: + entityType := payload.Entity.EntityType() + if entityType != model.EntityTypeTypeBatterySystem { + return + } + + switch payload.ChangeType { + case spine.ElementChangeAdd: + i.inverterConnected(payload.Ski, payload.Entity) + + case spine.ElementChangeRemove: + i.inverterDisconnected() + } + + case spine.EventTypeDataChange: + if payload.ChangeType != spine.ElementChangeUpdate { + return + } + + entityType := payload.Entity.EntityType() + if entityType != model.EntityTypeTypeBatterySystem { + return + } + + switch payload.Data.(type) { + case *model.ElectricalConnectionParameterDescriptionListDataType: + if i.inverterElectricalConnection == nil { + break + } + if _, err := i.inverterElectricalConnection.RequestPermittedValueSets(); err != nil { + logging.Log.Error("Error getting electrical permitted values:", err) + } + + case *model.ElectricalConnectionDescriptionListDataType: + if i.inverterElectricalConnection == nil { + break + } + if err := i.inverterElectricalConnection.RequestDescriptions(); err != nil { + logging.Log.Error("Error getting electrical permitted values:", err) + } + + case *model.MeasurementDescriptionListDataType: + if i.inverterMeasurement == nil { + break + } + if _, err := i.inverterMeasurement.RequestValues(); err != nil { + logging.Log.Error("Error getting measurement list values:", err) + } + } + } +} + +// process required steps when a battery device entity is connected +func (i *InverterBatteryVisImpl) inverterConnected(ski string, entity *spine.EntityRemoteImpl) { + i.inverterEntity = entity + localDevice := i.service.LocalDevice() + + f1, err := features.NewElectricalConnection(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) + if err != nil { + return + } + i.inverterElectricalConnection = f1 + + f2, err := features.NewMeasurement(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) + if err != nil { + return + } + i.inverterMeasurement = f2 + + // subscribe + if err := i.inverterElectricalConnection.SubscribeForEntity(); err != nil { + logging.Log.Error(err) + } + if err := i.inverterMeasurement.SubscribeForEntity(); err != nil { + logging.Log.Error(err) + } + + // get electrical connection parameter + if err := i.inverterElectricalConnection.RequestDescriptions(); err != nil { + logging.Log.Error(err) + } + + if err := i.inverterElectricalConnection.RequestParameterDescriptions(); err != nil { + logging.Log.Error(err) + } + + // get measurement parameters + if err := i.inverterMeasurement.RequestDescriptions(); err != nil { + logging.Log.Error(err) + } + + if err := i.inverterMeasurement.RequestConstraints(); err != nil { + logging.Log.Error(err) + } +} + +// a battery device entity was disconnected +func (i *InverterBatteryVisImpl) inverterDisconnected() { + i.inverterEntity = nil + + i.inverterElectricalConnection = nil + i.inverterMeasurement = nil +} diff --git a/inverterbatteryvis/invertervis.go b/inverterbatteryvis/invertervis.go new file mode 100644 index 0000000..d5aa5ae --- /dev/null +++ b/inverterbatteryvis/invertervis.go @@ -0,0 +1,45 @@ +package inverterbatteryvis + +import ( + "github.com/enbility/eebus-go/features" + "github.com/enbility/eebus-go/service" + "github.com/enbility/eebus-go/spine" + "github.com/enbility/eebus-go/util" +) + +type InverterBatteryVisI interface { + CurrentDisChargePower() (float64, error) + TotalChargeEnergy() (float64, error) + TotalDischargeEnergy() (float64, error) + CurrentStateOfCharge() (float64, error) +} + +type InverterBatteryVisImpl struct { + entity *spine.EntityLocalImpl + + service *service.EEBUSService + + inverterEntity *spine.EntityRemoteImpl + inverterElectricalConnection *features.ElectricalConnection + inverterMeasurement *features.Measurement + + ski string +} + +var _ InverterBatteryVisI = (*InverterBatteryVisImpl)(nil) + +// Add InverterBatteryVis support +func NewInverterBatteryVis(service *service.EEBUSService, details *service.ServiceDetails) *InverterBatteryVisImpl { + ski := util.NormalizeSKI(details.SKI()) + + inverter := &InverterBatteryVisImpl{ + service: service, + entity: service.LocalEntity(), + ski: ski, + } + spine.Events.Subscribe(inverter) + + service.PairRemoteService(details) + + return inverter +} diff --git a/inverterbatteryvis/public.go b/inverterbatteryvis/public.go new file mode 100644 index 0000000..339b099 --- /dev/null +++ b/inverterbatteryvis/public.go @@ -0,0 +1,118 @@ +package inverterbatteryvis + +import ( + "github.com/enbility/cemd/util" + "github.com/enbility/eebus-go/features" + "github.com/enbility/eebus-go/spine/model" +) + +// return the current battery (dis-)charge power (W) +// +// - positive values charge power +// - negative values discharge power +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (i *InverterBatteryVisImpl) CurrentDisChargePower() (float64, error) { + measurement := model.MeasurementTypeTypePower + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeACPowerTotal + + data, err := i.getValuesForTypeCommodityScope(measurement, commodity, scope) + if err != nil { + return 0, err + } + + // we assume there is only one value + mId := data[0].MeasurementId + value := data[0].Value + if mId == nil || value == nil { + return 0, features.ErrDataNotAvailable + } + + return value.GetValue(), nil +} + +// return the total charge energy (Wh) +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (i *InverterBatteryVisImpl) TotalChargeEnergy() (float64, error) { + measurement := model.MeasurementTypeTypeEnergy + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeCharge + data, err := i.getValuesForTypeCommodityScope(measurement, commodity, scope) + if err != nil { + return 0, err + } + + // we assume thre is only one result + value := data[0].Value + if value == nil { + return 0, features.ErrDataNotAvailable + } + + return value.GetValue(), nil +} + +// return the total discharge energy (Wh) +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (i *InverterBatteryVisImpl) TotalDischargeEnergy() (float64, error) { + measurement := model.MeasurementTypeTypeEnergy + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeDischarge + data, err := i.getValuesForTypeCommodityScope(measurement, commodity, scope) + if err != nil { + return 0, err + } + + // we assume thre is only one result + value := data[0].Value + if value == nil { + return 0, features.ErrDataNotAvailable + } + + return value.GetValue(), nil +} + +// return the current state of charge in % +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (i *InverterBatteryVisImpl) CurrentStateOfCharge() (float64, error) { + measurement := model.MeasurementTypeTypePercentage + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeStateOfCharge + data, err := i.getValuesForTypeCommodityScope(measurement, commodity, scope) + if err != nil { + return 0, err + } + + // we assume thre is only one result + value := data[0].Value + if value == nil { + return 0, features.ErrDataNotAvailable + } + + return value.GetValue(), nil +} + +// helper + +func (i *InverterBatteryVisImpl) getValuesForTypeCommodityScope(measurement model.MeasurementTypeType, commodity model.CommodityTypeType, scope model.ScopeTypeType) ([]model.MeasurementDataType, error) { + if i.inverterEntity == nil { + return nil, util.ErrDeviceDisconnected + } + + if i.inverterMeasurement == nil { + return nil, features.ErrDataNotAvailable + } + + return i.inverterMeasurement.GetValuesForTypeCommodityScope(measurement, commodity, scope) +} diff --git a/inverterbatteryvis/results.go b/inverterbatteryvis/results.go new file mode 100644 index 0000000..14eb3bb --- /dev/null +++ b/inverterbatteryvis/results.go @@ -0,0 +1,8 @@ +package inverterbatteryvis + +import ( + "github.com/enbility/eebus-go/spine" +) + +func (i *InverterBatteryVisImpl) HandleResult(errorMsg spine.ResultMessage) { +} diff --git a/inverterbatteryvis/scenario.go b/inverterbatteryvis/scenario.go new file mode 100644 index 0000000..f52c9fa --- /dev/null +++ b/inverterbatteryvis/scenario.go @@ -0,0 +1,95 @@ +package inverterbatteryvis + +import ( + "sync" + + "github.com/enbility/cemd/scenarios" + "github.com/enbility/eebus-go/service" + "github.com/enbility/eebus-go/spine" + "github.com/enbility/eebus-go/spine/model" +) + +type InverterBatteryVisScenarioImpl struct { + *scenarios.ScenarioImpl + + remoteDevices map[string]*InverterBatteryVisImpl + + mux sync.Mutex +} + +var _ scenarios.ScenariosI = (*InverterBatteryVisScenarioImpl)(nil) + +func NewInverterVisScenario(service *service.EEBUSService) *InverterBatteryVisScenarioImpl { + return &InverterBatteryVisScenarioImpl{ + ScenarioImpl: scenarios.NewScenarioImpl(service), + remoteDevices: make(map[string]*InverterBatteryVisImpl), + } +} + +// adds all the supported features to the local entity +func (i *InverterBatteryVisScenarioImpl) AddFeatures() { + localEntity := i.Service.LocalEntity() + + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + } + + for _, feature := range clientFeatures { + f := localEntity.GetOrAddFeature(feature, model.RoleTypeClient) + f.AddResultHandler(i) + } +} + +// add supported inverter usecases +func (i *InverterBatteryVisScenarioImpl) AddUseCases() { + localEntity := i.Service.LocalEntity() + + _ = spine.NewUseCaseWithActor( + localEntity, + model.UseCaseActorTypeVisualizationAppliance, + model.UseCaseNameTypeVisualizationOfAggregatedBatteryData, + model.SpecificationVersionType("1.0.0 RC1"), + []model.UseCaseScenarioSupportType{1, 2, 3, 4}) +} + +func (i *InverterBatteryVisScenarioImpl) RegisterRemoteDevice(details *service.ServiceDetails, dataProvider any) any { + // TODO: invertervis should be stored per remote SKI and + // only be set for the SKI if the device supports it + i.mux.Lock() + defer i.mux.Unlock() + + if em, ok := i.remoteDevices[details.SKI()]; ok { + return em + } + + inverter := NewInverterBatteryVis(i.Service, details) + i.remoteDevices[details.SKI()] = inverter + return inverter +} + +func (i *InverterBatteryVisScenarioImpl) UnRegisterRemoteDevice(remoteDeviceSki string) error { + i.mux.Lock() + defer i.mux.Unlock() + + delete(i.remoteDevices, remoteDeviceSki) + + return i.Service.UnpairRemoteService(remoteDeviceSki) +} + +func (i *InverterBatteryVisScenarioImpl) HandleResult(errorMsg spine.ResultMessage) { + i.mux.Lock() + defer i.mux.Unlock() + + if errorMsg.DeviceRemote == nil { + return + } + + em, ok := i.remoteDevices[errorMsg.DeviceRemote.Ski()] + if !ok { + return + } + + em.HandleResult(errorMsg) +} diff --git a/inverterpvvis/events.go b/inverterpvvis/events.go new file mode 100644 index 0000000..a069efa --- /dev/null +++ b/inverterpvvis/events.go @@ -0,0 +1,156 @@ +package inverterpvvis + +import ( + "github.com/enbility/eebus-go/features" + "github.com/enbility/eebus-go/logging" + "github.com/enbility/eebus-go/spine" + "github.com/enbility/eebus-go/spine/model" +) + +// Internal EventHandler Interface for the CEM +func (i *InverterPVVisImpl) HandleEvent(payload spine.EventPayload) { + // we only care about the registered SKI + if payload.Ski != i.ski { + return + } + + // we care only about events for this remote device + if payload.Device != nil && payload.Device.Ski() != i.ski { + return + } + + switch payload.EventType { + case spine.EventTypeDeviceChange: + switch payload.ChangeType { + case spine.ElementChangeRemove: + i.inverterDisconnected() + } + + case spine.EventTypeEntityChange: + entityType := payload.Entity.EntityType() + if entityType != model.EntityTypeTypeBatterySystem { + return + } + + switch payload.ChangeType { + case spine.ElementChangeAdd: + i.inverterConnected(payload.Ski, payload.Entity) + + case spine.ElementChangeRemove: + i.inverterDisconnected() + } + + case spine.EventTypeDataChange: + if payload.ChangeType != spine.ElementChangeUpdate { + return + } + + entityType := payload.Entity.EntityType() + if entityType != model.EntityTypeTypeBatterySystem { + return + } + + switch payload.Data.(type) { + case *model.DeviceConfigurationKeyValueDescriptionListDataType: + if i.inverterDeviceConfiguration == nil { + break + } + + // key value descriptions received, now get the data + if _, err := i.inverterDeviceConfiguration.RequestKeyValues(); err != nil { + logging.Log.Error("Error getting configuration key values:", err) + } + + case *model.ElectricalConnectionParameterDescriptionListDataType: + if i.inverterElectricalConnection == nil { + break + } + if _, err := i.inverterElectricalConnection.RequestPermittedValueSets(); err != nil { + logging.Log.Error("Error getting electrical permitted values:", err) + } + + case *model.ElectricalConnectionDescriptionListDataType: + if i.inverterElectricalConnection == nil { + break + } + if err := i.inverterElectricalConnection.RequestDescriptions(); err != nil { + logging.Log.Error("Error getting electrical permitted values:", err) + } + + case *model.MeasurementDescriptionListDataType: + if i.inverterMeasurement == nil { + break + } + if _, err := i.inverterMeasurement.RequestValues(); err != nil { + logging.Log.Error("Error getting measurement list values:", err) + } + } + } +} + +// process required steps when a pv device entity is connected +func (e *InverterPVVisImpl) inverterConnected(ski string, entity *spine.EntityRemoteImpl) { + e.inverterEntity = entity + localDevice := e.service.LocalDevice() + + f1, err := features.NewElectricalConnection(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) + if err != nil { + return + } + e.inverterElectricalConnection = f1 + + f2, err := features.NewMeasurement(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) + if err != nil { + return + } + e.inverterMeasurement = f2 + + f3, err := features.NewDeviceConfiguration(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) + if err != nil { + return + } + e.inverterDeviceConfiguration = f3 + + // subscribe + if err := e.inverterDeviceConfiguration.SubscribeForEntity(); err != nil { + logging.Log.Error(err) + } + if err := e.inverterElectricalConnection.SubscribeForEntity(); err != nil { + logging.Log.Error(err) + } + if err := e.inverterMeasurement.SubscribeForEntity(); err != nil { + logging.Log.Error(err) + } + + // get device configuration data + if err := e.inverterDeviceConfiguration.RequestDescriptions(); err != nil { + logging.Log.Error(err) + } + + // get electrical connection parameter + if err := e.inverterElectricalConnection.RequestDescriptions(); err != nil { + logging.Log.Error(err) + } + + if err := e.inverterElectricalConnection.RequestParameterDescriptions(); err != nil { + logging.Log.Error(err) + } + + // get measurement parameters + if err := e.inverterMeasurement.RequestDescriptions(); err != nil { + logging.Log.Error(err) + } + + if err := e.inverterMeasurement.RequestConstraints(); err != nil { + logging.Log.Error(err) + } +} + +// a pv device entity was disconnected +func (e *InverterPVVisImpl) inverterDisconnected() { + e.inverterMeasurement = nil + + e.inverterElectricalConnection = nil + e.inverterMeasurement = nil + e.inverterDeviceConfiguration = nil +} diff --git a/inverterpvvis/invertervis.go b/inverterpvvis/invertervis.go new file mode 100644 index 0000000..3706db0 --- /dev/null +++ b/inverterpvvis/invertervis.go @@ -0,0 +1,45 @@ +package inverterpvvis + +import ( + "github.com/enbility/eebus-go/features" + "github.com/enbility/eebus-go/service" + "github.com/enbility/eebus-go/spine" + "github.com/enbility/eebus-go/util" +) + +type InverterPVVisI interface { + CurrentProductionPower() (float64, error) + NominalPeakPower() (float64, error) + TotalPVYield() (float64, error) +} + +type InverterPVVisImpl struct { + entity *spine.EntityLocalImpl + + service *service.EEBUSService + + inverterEntity *spine.EntityRemoteImpl + inverterDeviceConfiguration *features.DeviceConfiguration + inverterElectricalConnection *features.ElectricalConnection + inverterMeasurement *features.Measurement + + ski string +} + +var _ InverterPVVisI = (*InverterPVVisImpl)(nil) + +// Add InverterPVVis support +func NewInverterPVVis(service *service.EEBUSService, details *service.ServiceDetails) *InverterPVVisImpl { + ski := util.NormalizeSKI(details.SKI()) + + inverter := &InverterPVVisImpl{ + service: service, + entity: service.LocalEntity(), + ski: ski, + } + spine.Events.Subscribe(inverter) + + service.PairRemoteService(details) + + return inverter +} diff --git a/inverterpvvis/public.go b/inverterpvvis/public.go new file mode 100644 index 0000000..2db1d5c --- /dev/null +++ b/inverterpvvis/public.go @@ -0,0 +1,106 @@ +package inverterpvvis + +import ( + "github.com/enbility/cemd/util" + "github.com/enbility/eebus-go/features" + "github.com/enbility/eebus-go/spine/model" +) + +// return the current photovoltaic production power (W) +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (i *InverterPVVisImpl) CurrentProductionPower() (float64, error) { + measurement := model.MeasurementTypeTypePower + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeACPowerTotal + + data, err := i.getValuesForTypeCommodityScope(measurement, commodity, scope) + if err != nil { + return 0, err + } + + // we assume there is only one value + mId := data[0].MeasurementId + value := data[0].Value + if mId == nil || value == nil { + return 0, features.ErrDataNotAvailable + } + + return value.GetValue(), nil +} + +// return the nominal photovoltaic peak power (W) +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (i *InverterPVVisImpl) NominalPeakPower() (float64, error) { + if i.inverterEntity == nil { + return 0, util.ErrDeviceDisconnected + } + + if i.inverterDeviceConfiguration == nil { + return 0, features.ErrDataNotAvailable + } + + _, err := i.inverterDeviceConfiguration.GetDescriptionForKeyName(model.DeviceConfigurationKeyNameTypePeakPowerOfPVSystem) + if err != nil { + return 0, err + } + + data, err := i.inverterDeviceConfiguration.GetKeyValueForKeyName(model.DeviceConfigurationKeyNameTypePeakPowerOfPVSystem, model.DeviceConfigurationKeyValueTypeTypeScaledNumber) + if err != nil { + return 0, err + } + + if data == nil { + return 0, features.ErrDataNotAvailable + } + + value := data.(*model.ScaledNumberType) + + if value == nil { + return 0, features.ErrDataNotAvailable + } + + return value.GetValue(), nil +} + +// return the total photovoltaic yield (Wh) +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (i *InverterPVVisImpl) TotalPVYield() (float64, error) { + measurement := model.MeasurementTypeTypeEnergy + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeACYieldTotal + data, err := i.getValuesForTypeCommodityScope(measurement, commodity, scope) + if err != nil { + return 0, err + } + + // we assume thre is only one result + value := data[0].Value + if value == nil { + return 0, features.ErrDataNotAvailable + } + + return value.GetValue(), nil +} + +// helper + +func (i *InverterPVVisImpl) getValuesForTypeCommodityScope(measurement model.MeasurementTypeType, commodity model.CommodityTypeType, scope model.ScopeTypeType) ([]model.MeasurementDataType, error) { + if i.inverterEntity == nil { + return nil, util.ErrDeviceDisconnected + } + + if i.inverterMeasurement == nil { + return nil, features.ErrDataNotAvailable + } + + return i.inverterMeasurement.GetValuesForTypeCommodityScope(measurement, commodity, scope) +} diff --git a/inverterpvvis/results.go b/inverterpvvis/results.go new file mode 100644 index 0000000..1d02bbd --- /dev/null +++ b/inverterpvvis/results.go @@ -0,0 +1,8 @@ +package inverterpvvis + +import ( + "github.com/enbility/eebus-go/spine" +) + +func (i *InverterPVVisImpl) HandleResult(errorMsg spine.ResultMessage) { +} diff --git a/inverterpvvis/scenario.go b/inverterpvvis/scenario.go new file mode 100644 index 0000000..b6a3271 --- /dev/null +++ b/inverterpvvis/scenario.go @@ -0,0 +1,95 @@ +package inverterpvvis + +import ( + "sync" + + "github.com/enbility/cemd/scenarios" + "github.com/enbility/eebus-go/service" + "github.com/enbility/eebus-go/spine" + "github.com/enbility/eebus-go/spine/model" +) + +type InverterPVVisScenarioImpl struct { + *scenarios.ScenarioImpl + + remoteDevices map[string]*InverterPVVisImpl + + mux sync.Mutex +} + +var _ scenarios.ScenariosI = (*InverterPVVisScenarioImpl)(nil) + +func NewInverterVisScenario(service *service.EEBUSService) *InverterPVVisScenarioImpl { + return &InverterPVVisScenarioImpl{ + ScenarioImpl: scenarios.NewScenarioImpl(service), + remoteDevices: make(map[string]*InverterPVVisImpl), + } +} + +// adds all the supported features to the local entity +func (i *InverterPVVisScenarioImpl) AddFeatures() { + localEntity := i.Service.LocalEntity() + + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + } + + for _, feature := range clientFeatures { + f := localEntity.GetOrAddFeature(feature, model.RoleTypeClient) + f.AddResultHandler(i) + } +} + +// add supported inverter usecases +func (i *InverterPVVisScenarioImpl) AddUseCases() { + localEntity := i.Service.LocalEntity() + + _ = spine.NewUseCaseWithActor( + localEntity, + model.UseCaseActorTypeVisualizationAppliance, + model.UseCaseNameTypeVisualizationOfAggregatedPhotovoltaicData, + model.SpecificationVersionType("1.0.0 RC1"), + []model.UseCaseScenarioSupportType{1, 2, 3}) +} + +func (i *InverterPVVisScenarioImpl) RegisterRemoteDevice(details *service.ServiceDetails, dataProvider any) any { + // TODO: invertervis should be stored per remote SKI and + // only be set for the SKI if the device supports it + i.mux.Lock() + defer i.mux.Unlock() + + if em, ok := i.remoteDevices[details.SKI()]; ok { + return em + } + + inverter := NewInverterPVVis(i.Service, details) + i.remoteDevices[details.SKI()] = inverter + return inverter +} + +func (i *InverterPVVisScenarioImpl) UnRegisterRemoteDevice(remoteDeviceSki string) error { + i.mux.Lock() + defer i.mux.Unlock() + + delete(i.remoteDevices, remoteDeviceSki) + + return i.Service.UnpairRemoteService(remoteDeviceSki) +} + +func (i *InverterPVVisScenarioImpl) HandleResult(errorMsg spine.ResultMessage) { + i.mux.Lock() + defer i.mux.Unlock() + + if errorMsg.DeviceRemote == nil { + return + } + + em, ok := i.remoteDevices[errorMsg.DeviceRemote.Ski()] + if !ok { + return + } + + em.HandleResult(errorMsg) +} diff --git a/scenarios/types.go b/scenarios/types.go index e3121ca..ecc9a5f 100644 --- a/scenarios/types.go +++ b/scenarios/types.go @@ -1,9 +1,13 @@ package scenarios -import "github.com/enbility/eebus-go/service" +import ( + "github.com/enbility/eebus-go/service" +) -// Implemented by EmobilityScenarioImpl, used by CemImpl +// Implemented by *ScenarioImpl, used by CemImpl type ScenariosI interface { + RegisterRemoteDevice(details *service.ServiceDetails, dataProvider any) any + UnRegisterRemoteDevice(remoteDeviceSki string) error AddFeatures() AddUseCases() } diff --git a/util/errors.go b/util/errors.go new file mode 100644 index 0000000..6535021 --- /dev/null +++ b/util/errors.go @@ -0,0 +1,5 @@ +package util + +import "errors" + +var ErrDeviceDisconnected = errors.New("device is disconnected") diff --git a/util/helper.go b/util/helper.go index bda597d..b2f7b04 100644 --- a/util/helper.go +++ b/util/helper.go @@ -7,6 +7,8 @@ import ( "github.com/enbility/eebus-go/spine/model" ) +var PhaseNameMapping = []model.ElectricalConnectionPhaseNameType{model.ElectricalConnectionPhaseNameTypeA, model.ElectricalConnectionPhaseNameTypeB, model.ElectricalConnectionPhaseNameTypeC} + // check if the given usecase, actor is supported by the remote device func IsUsecaseSupported(usecase model.UseCaseNameType, actor model.UseCaseActorType, remoteDevice *spine.DeviceRemoteImpl) bool { uci := remoteDevice.UseCaseManager().UseCaseInformation()