diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index 86c198c..e0c42ae 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -15,10 +15,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: ^1.18 @@ -26,16 +26,30 @@ jobs: run: go build -v ./... - name: Lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@master with: version: latest skip-pkg-cache: true skip-build-cache: true - name: Test - run: go test -race -v -coverprofile=coverage.out -covermode=atomic ./... + run: go test -race -v -coverprofile=coverage_temp.out -covermode=atomic ./... + + - name: Remove mocks and cmd from coverage + run: grep -v -e "/cemd/mocks/" -e "/cemd/cmd/" coverage_temp.out > coverage.out - name: Send coverage - uses: shogo82148/actions-goveralls@v1 + uses: coverallsapp/github-action@v2 + with: + file: coverage.out + + - name: Run Gosec Security Scanner + uses: securego/gosec@master + with: + # we let the report trigger content trigger a failure using the GitHub Security features. + args: '-no-fail -fmt sarif -out results.sarif ./...' + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@v3 with: - path-to-profile: coverage.out \ No newline at end of file + # Path to SARIF file relative to the root of the repository + sarif_file: results.sarif \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..dd9bcca --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,58 @@ +run: + # timeout for analysis, e.g. 30s, 5m, default is 1m + timeout: 5m + + # include test files or not, default is true + tests: true + + # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules": + # If invoked with -mod=readonly, the go command is disallowed from the implicit + # automatic updating of go.mod described above. Instead, it fails when any changes + # to go.mod are needed. This setting is most useful to check that go.mod does + # not need updates, such as in a continuous integration and testing system. + # If invoked with -mod=vendor, the go command assumes that the vendor + # directory holds the correct copies of dependencies and ignores + # the dependency descriptions in go.mod. + modules-download-mode: readonly + +# output configuration options +output: + # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number" + formats: + - format: colored-line-number + +linters: + enable: + - bodyclose + - errcheck + - errorlint + - gocheckcompilerdirectives + - gochecknoinits + - gochecksumtype + - goconst + - gofmt + - gosimple + - gosec + - govet + - nilerr + - nilnil + - staticcheck + - typecheck + - unused + - whitespace + +issues: + # Excluding configuration per-path, per-linter, per-text and per-source + exclude-rules: + # Exclude some linters from running on tests files. + - path: _test\.go + linters: + - errcheck + - goconst + - gosec + + # checking for errors in defers seldom makes sense... + - source: "^\\s*defer\\s" + linters: + - errcheck + - staticcheck diff --git a/.mockery.yaml b/.mockery.yaml new file mode 100644 index 0000000..29eacfc --- /dev/null +++ b/.mockery.yaml @@ -0,0 +1,25 @@ +with-expecter: true +inpackage: false +dir: "{{ .InterfaceDir }}/../mocks" +mockname: "{{.InterfaceName}}" +outpkg: "mocks" +filename: "{{.InterfaceName}}.go" +all: true +packages: + github.com/enbility/cemd/api: + github.com/enbility/cemd/cem: + github.com/enbility/cemd/uccevc: + github.com/enbility/cemd/ucevcc: + github.com/enbility/cemd/ucevcem: + github.com/enbility/cemd/ucevsecc: + github.com/enbility/cemd/ucevsoc: + github.com/enbility/cemd/uclpc: + github.com/enbility/cemd/uclpcserver: + github.com/enbility/cemd/uclpp: + github.com/enbility/cemd/uclppserver: + github.com/enbility/cemd/ucmgcp: + github.com/enbility/cemd/ucmpc: + github.com/enbility/cemd/ucopev: + github.com/enbility/cemd/ucoscev: + github.com/enbility/cemd/ucvabd: + github.com/enbility/cemd/ucvapd: diff --git a/LICENSE b/LICENSE index 5453335..beae98e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Andreas Linde +Copyright (c) 2022-2024 Andreas Linde Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index ea03fc3..6a8c814 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # cemd -[![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) +[![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) [![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) +[![Coverage Status](https://coveralls.io/repos/github/enbility/cemd/badge.svg?branch=main)](https://coveralls.io/github/enbility/cemd?branch=main) [![Go report](https://goreportcard.com/badge/github.com/enbility/cemd)](https://goreportcard.com/report/github.com/enbility/cemd) +[![CodeFactor](https://www.codefactor.io/repository/github/enbility/cemd/badge)](https://www.codefactor.io/repository/github/enbility/cemd) The goal is to provide an EEBUS CEM implementation @@ -11,33 +12,35 @@ The goal is to provide an EEBUS CEM implementation This library provides a foundation to implement energy management solutions using the [eebus-go](https://github.com/enbility/eebus-go) library. It is designed to be included either directly into go projects, or it will be able to run as a daemon for other systems interact with (to be implemented). -These EEBUS use cases are already supported: - -- E-Mobility: - - - EVSE Commissioning and Configuration V1.0.1 - - EV Commissioning and Configuration V1.0.1 - - EV Charging Electricity Measurement V1.0.1 - - EV State Of Charge V1.0.0 RC1 - - Optimization of Self Consumption During EV Charging V1.0.1b - - Overload Protection by EV Charging Current Curtailment V1.0.1b - -These use cases are currently planned to be supported in the future: - -- E-Mobility: - - - Coordinated EV Charging V1.0.1 - - EV Charging Summary V1.0.1 - -More use cases and scenarios will hopefully follow in the future as well. +## Packages + +- `api`: API interface definitions +- `cem`: Central CEM implementation which needs to be used by a HEMS implementation +- `cmd`: Example project +- `uccevc`: Use Case Coordinated EV Charging V1.0.1 +- `ucevcc`: Use Case EV Commissioning and Configuration V1.0.1 +- `ucevcem`: Use Case EV Charging Electricity Measurement V1.0.1 +- `ucevsecc`: Use Case EVSE Commissioning and Configuration V1.0.1 +- `ucevsoc`: Use Case EV State Of Charge V1.0.0 RC1 +- `uclpc`: Use Case Limitation of Power Consumption V1.0.0 as a Energy Guard +- `uclpcserver`: Use Case Limitation of Power Consumption V1.0.0 as a Controllable System +- `ucmgcp`: Use Case Monitoring of Grid Connection Point V1.0.0 +- `ucmpc`: Use Case Monitoring of Power Consumption V1.0.0 as a Monitoring Appliance +- `ucopev`: Use Case Overload Protection by EV Charging Current Curtailment V1.0.1b +- `ucoscev`: Use Case Optimization of Self Consumption During EV Charging V1.0.1b +- `ucvabd`: Use Case Visualization of Aggregated Battery Data V1.0.0 RC1 as a Visualization Appliance +- `ucvapd`: Use Case Visualization of Aggregated Photovoltaic Data V1.0.0 RC1 as a Visualization Appliance +- `util`: various internal helpers ## Usage +Run the following command to see all the options: + ```sh -Usage: go run cmd/main.go +Usage: go run cmd/main.go ``` -Example certificate and key files are located in the keys folder +Example certificate and key files are located in the keys folder. If no certificate and key are provided in the options, new ones will be generated in the current folder. ### Explanation diff --git a/api/api.go b/api/api.go new file mode 100644 index 0000000..315dee1 --- /dev/null +++ b/api/api.go @@ -0,0 +1,70 @@ +package api + +import ( + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +//go:generate mockery + +// Device event callback +// +// Used by CEM implementation +type DeviceEventCallback func(ski string, device spineapi.DeviceRemoteInterface, event EventType) + +// Entity event callback +// +// Used by Use Case implementations +type EntityEventCallback func(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event EventType) + +// Implemented by CEM +type CemInterface interface { + // Setup the EEBUS service + Setup() error + + // Start the EEBUS service + Start() + + // Shutdown the EEBUS service + Shutdown() + + // Add a use case implementation + AddUseCase(usecase UseCaseInterface) +} + +// Implemented by each Use Case +type UseCaseInterface interface { + // provide the usecase name + UseCaseName() model.UseCaseNameType + + // add the features + AddFeatures() + + // add the use case + AddUseCase() + + // update availability of the use case + UpdateUseCaseAvailability(available bool) + + // returns if the entity supports the usecase + // + // possible errors: + // - ErrDataNotAvailable if that information is not (yet) available + // - and others + IsUseCaseSupported(remoteEntity spineapi.EntityRemoteInterface) (bool, error) +} + +type ManufacturerData struct { + DeviceName string `json:"deviceName,omitempty"` + DeviceCode string `json:"deviceCode,omitempty"` + SerialNumber string `json:"serialNumber,omitempty"` + SoftwareRevision string `json:"softwareRevision,omitempty"` + HardwareRevision string `json:"hardwareRevision,omitempty"` + VendorName string `json:"vendorName,omitempty"` + VendorCode string `json:"vendorCode,omitempty"` + BrandName string `json:"brandName,omitempty"` + PowerSource string `json:"powerSource,omitempty"` + ManufacturerNodeIdentification string `json:"manufacturerNodeIdentification,omitempty"` + ManufacturerLabel string `json:"manufacturerLabel,omitempty"` + ManufacturerDescription string `json:"manufacturerDescription,omitempty"` +} diff --git a/api/types.go b/api/types.go new file mode 100644 index 0000000..a54f1ce --- /dev/null +++ b/api/types.go @@ -0,0 +1,157 @@ +package api + +import ( + "errors" + "time" + + "github.com/enbility/spine-go/model" +) + +type EVChargeStateType string + +const ( + EVChargeStateTypeUnknown EVChargeStateType = "Unknown" + EVChargeStateTypeUnplugged EVChargeStateType = "unplugged" + EVChargeStateTypeError EVChargeStateType = "error" + EVChargeStateTypePaused EVChargeStateType = "paused" + EVChargeStateTypeActive EVChargeStateType = "active" + EVChargeStateTypeFinished EVChargeStateType = "finished" +) + +// Defines a phase specific limit data set +type LoadLimitsPhase struct { + Phase model.ElectricalConnectionPhaseNameType // the phase + IsChangeable bool // if the value can be changed via write, ignored when writing data + IsActive bool // if the limit is active + Value float64 // the limit in A +} + +// Defines a limit data set +type LoadLimit struct { + Duration time.Duration // the duration of the limit, + IsChangeable bool // if the value can be changed via write, ignored when writing data + IsActive bool // if the limit is active + Value float64 // the limit in A +} + +// identification +type IdentificationItem struct { + // the identification value + Value string + + // the type of the identification value, e.g. + ValueType model.IdentificationTypeType +} + +type EVChargeStrategyType string + +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 Demand 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 float64 // the duration in s from now until charging will start, this could be in the future but usualy is now + DurationUntilEnd float64 // the duration in s from now until minDemand or optDemand has to be reached, 0 if direct charge strategy is active +} + +// Contains details about an EV generated charging plan +type ChargePlan struct { + Slots []ChargePlanSlotValue // Individual charging slot details +} + +// Contains details about a charging plan slot +type ChargePlanSlotValue struct { + Start time.Time // The start time of the slot + End time.Time // The duration of the slot + Value float64 // planned power value + MinValue float64 // minimum power value + MaxValue float64 // maximum power value +} + +// Details about the time slot constraints +type TimeSlotConstraints 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 IncentiveSlotConstraints struct { + MinSlots uint // the minimum number of slots, no minimum if 0 + MaxSlots uint // the maximum number of slots, unlimited if 0 +} + +// details about the boundary +type TierBoundaryDescription struct { + // the id of the boundary + Id uint + + // the type of the boundary + Type model.TierBoundaryTypeType + + // the unit of the boundary + Unit model.UnitOfMeasurementType +} + +// details about incentive +type IncentiveDescription struct { + // the id of the incentive + Id uint + + // the type of the incentive + Type model.IncentiveTypeType + + // the currency of the incentive, if it is price based + Currency model.CurrencyType +} + +// Contains about one tier in a tariff +type IncentiveTableDescriptionTier struct { + // the id of the tier + Id uint + + // the tiers type + Type model.TierTypeType + + // each tear has 1 to 3 boundaries + // used for different power limits, e.g. 0-1kW x€, 1-3kW y€, ... + Boundaries []TierBoundaryDescription + + // each tier has 1 to 3 incentives + // - price/costs (absolute or relative) + // - renewable energy percentage + // - CO2 emissions + Incentives []IncentiveDescription +} + +// Contains details about a tariff +type IncentiveTariffDescription struct { + // each tariff can have 1 to 3 tiers + Tiers []IncentiveTableDescriptionTier +} + +// Contains details about power limits or incentives for a defined timeframe +type DurationSlotValue struct { + Duration time.Duration // Duration of this slot + Value float64 // Energy Cost or Power Limit +} + +// type for cem and usecase specfic event names +type EventType string + +var ErrNoCompatibleEntity = errors.New("entity is not an compatible entity") diff --git a/cem/cem.go b/cem/cem.go index e7d70bc..d5c6065 100644 --- a/cem/cem.go +++ b/cem/cem.go @@ -1,121 +1,64 @@ 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/cemd/api" + eebusapi "github.com/enbility/eebus-go/api" "github.com/enbility/eebus-go/service" - "github.com/enbility/eebus-go/spine" - "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/ship-go/logging" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" ) // Generic CEM implementation -type CemImpl struct { - service *service.EEBUSService - - emobilityScenario, gridScenario, inverterBatteryVisScenario, inverterPVVisScenario scenarios.ScenariosI +type Cem struct { + Service eebusapi.ServiceInterface Currency model.CurrencyType -} - -func NewCEM(serviceDescription *service.Configuration, serviceHandler service.EEBUSServiceHandler, log logging.Logging) *CemImpl { - cem := &CemImpl{ - service: service.NewEEBUSService(serviceDescription, serviceHandler), - Currency: model.CurrencyTypeEur, - } - cem.service.SetLogging(log) + eventCB api.DeviceEventCallback - return cem + usecases []api.UseCaseInterface } -// Set up the supported usecases and features -func (h *CemImpl) Setup() error { - if err := h.service.Setup(); err != nil { - return err +func NewCEM( + serviceDescription *eebusapi.Configuration, + serviceHandler eebusapi.ServiceReaderInterface, + eventCB api.DeviceEventCallback, + log logging.LoggingInterface) *Cem { + cem := &Cem{ + Service: service.NewService(serviceDescription, serviceHandler), + Currency: model.CurrencyTypeEur, + eventCB: eventCB, } - spine.Events.Subscribe(h) + cem.Service.SetLogging(log) - 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() -} + _ = spine.Events.Subscribe(cem) -func (h *CemImpl) EnablePVVisualization() { - h.inverterPVVisScenario = inverterpvvis.NewInverterVisScenario(h.service) - h.inverterPVVisScenario.AddFeatures() - h.inverterPVVisScenario.AddUseCases() -} - -func (h *CemImpl) Start() { - h.service.Start() -} - -func (h *CemImpl) Shutdown() { - h.service.Shutdown() -} - -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.UnRegisterRemoteDevice(remoteDeviceSki) + return cem } -func (h *CemImpl) RegisterGridRemoteDevice(details *service.ServiceDetails) *grid.GridImpl { - impl := h.gridScenario.RegisterRemoteDevice(details, nil) - return impl.(*grid.GridImpl) -} +var _ api.CemInterface = (*Cem)(nil) -func (h *CemImpl) UnRegisterGridRemoteDevice(remoteDeviceSki string) error { - return h.gridScenario.UnRegisterRemoteDevice(remoteDeviceSki) +// Set up the eebus service +func (h *Cem) Setup() error { + return h.Service.Setup() } -func (h *CemImpl) RegisterInverterBatteryVisRemoteDevice(details *service.ServiceDetails) *grid.GridImpl { - impl := h.inverterBatteryVisScenario.RegisterRemoteDevice(details, nil) - return impl.(*grid.GridImpl) +// Start the EEBUS service +func (h *Cem) Start() { + h.Service.Start() } -func (h *CemImpl) UnRegisterInverterBatteryVisRemoteDevice(remoteDeviceSki string) error { - return h.inverterBatteryVisScenario.UnRegisterRemoteDevice(remoteDeviceSki) +// Shutdown the EEBUS servic +func (h *Cem) Shutdown() { + h.Service.Shutdown() } -func (h *CemImpl) RegisterInverterPVVisRemoteDevice(details *service.ServiceDetails) *grid.GridImpl { - impl := h.inverterPVVisScenario.RegisterRemoteDevice(details, nil) - return impl.(*grid.GridImpl) -} +// Add a use case implementation +func (h *Cem) AddUseCase(usecase api.UseCaseInterface) { + h.usecases = append(h.usecases, usecase) -func (h *CemImpl) UnRegisterInverterPVVisRemoteDevice(remoteDeviceSki string) error { - return h.inverterPVVisScenario.UnRegisterRemoteDevice(remoteDeviceSki) + usecase.AddFeatures() + usecase.AddUseCase() } diff --git a/cem/cem_test.go b/cem/cem_test.go new file mode 100644 index 0000000..35a43eb --- /dev/null +++ b/cem/cem_test.go @@ -0,0 +1,92 @@ +package cem + +import ( + "testing" + "time" + + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/ucevsecc" + eebusapi "github.com/enbility/eebus-go/api" + shipapi "github.com/enbility/ship-go/api" + "github.com/enbility/ship-go/cert" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +func TestCemSuite(t *testing.T) { + suite.Run(t, new(CemSuite)) +} + +type CemSuite struct { + suite.Suite + + sut *Cem + mockRemoteDevice *mocks.DeviceRemoteInterface +} + +func (s *CemSuite) BeforeTest(suiteName, testName string) { + s.mockRemoteDevice = mocks.NewDeviceRemoteInterface(s.T()) + + certificate, err := cert.CreateCertificate("Demo", "Demo", "DE", "Demo-Unit-10") + assert.Nil(s.T(), err) + + configuration, err := eebusapi.NewConfiguration( + "Demo", + "Demo", + "HEMS", + "123456789", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 7654, + certificate, + 230, + time.Second*4) + assert.Nil(s.T(), err) + + noLogging := &logging.NoLogging{} + s.sut = NewCEM(configuration, s, s.deviceEventCB, noLogging) + assert.NotNil(s.T(), s.sut) +} +func (s *CemSuite) Test_CEM() { + err := s.sut.Setup() + assert.Nil(s.T(), err) + + ucEvseCC := ucevsecc.NewUCEVSECC(s.sut.Service, s.entityEventCB) + s.sut.AddUseCase(ucEvseCC) + + s.sut.Start() + s.sut.Shutdown() +} + +// Callbacks +func (d *CemSuite) deviceEventCB(ski string, device spineapi.DeviceRemoteInterface, event api.EventType) { +} + +func (d *CemSuite) entityEventCB(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { +} + +// eebusapi.ServiceReaderInterface + +// report the Ship ID of a newly trusted connection +func (d *CemSuite) RemoteServiceShipIDReported(service eebusapi.ServiceInterface, ski string, shipID string) { + // we should associated the Ship ID with the SKI and store it + // so the next connection can start trusted + logging.Log().Info("SKI", ski, "has Ship ID:", shipID) +} + +func (d *CemSuite) RemoteSKIConnected(service eebusapi.ServiceInterface, ski string) {} + +func (d *CemSuite) RemoteSKIDisconnected(service eebusapi.ServiceInterface, ski string) {} + +func (d *CemSuite) VisibleRemoteServicesUpdated(service eebusapi.ServiceInterface, entries []shipapi.RemoteService) { +} + +func (h *CemSuite) ServiceShipIDUpdate(ski string, shipdID string) {} + +func (h *CemSuite) ServicePairingDetailUpdate(ski string, detail *shipapi.ConnectionStateDetail) {} + +func (h *CemSuite) AllowWaitingForTrust(ski string) bool { return true } diff --git a/cem/events.go b/cem/events.go index 3961f5d..a2f1ebc 100644 --- a/cem/events.go +++ b/cem/events.go @@ -1,50 +1,19 @@ package cem import ( - "github.com/enbility/eebus-go/logging" - "github.com/enbility/eebus-go/spine" - "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/cemd/util" + spineapi "github.com/enbility/spine-go/api" ) -// Handle events from eebus-go library -func (h *CemImpl) HandleEvent(payload spine.EventPayload) { - switch payload.EventType { - case spine.EventTypeSubscriptionChange: - switch payload.Data.(type) { - case model.SubscriptionManagementRequestCallType: - h.subscriptionRequestHandling(payload) - } - } -} - -// Handle subscription requests -func (h *CemImpl) subscriptionRequestHandling(payload spine.EventPayload) { - data := payload.Data.(model.SubscriptionManagementRequestCallType) - - // Heartbeat subscription requests? - if *data.ServerFeatureType != model.FeatureTypeTypeDeviceDiagnosis { +// handle SPINE events +func (h *Cem) HandleEvent(payload spineapi.EventPayload) { + if util.IsDeviceConnected(payload) { + h.eventCB(payload.Ski, payload.Device, DeviceConnected) return } - remoteDevice := h.service.RemoteDeviceForSki(payload.Ski) - if remoteDevice == nil { - logging.Log.Info("No remote device found for SKI:", payload.Ski) + if util.IsDeviceDisconnected(payload) { + h.eventCB(payload.Ski, payload.Device, DeviceDisconnected) return } - - senderAddr := h.service.LocalDevice().FeatureByTypeAndRole(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer).Address() - destinationAddr := payload.Feature.Address() - if senderAddr == nil || destinationAddr == nil { - logging.Log.Info("No sender or destination address found for SKI:", payload.Ski) - return - } - - switch payload.ChangeType { - case spine.ElementChangeAdd: - // start sending heartbeats - remoteDevice.StartHeartbeatSend(senderAddr, destinationAddr) - case spine.ElementChangeRemove: - // stop sending heartbeats - remoteDevice.Stopheartbeat() - } } diff --git a/cem/events_test.go b/cem/events_test.go new file mode 100644 index 0000000..2cbf322 --- /dev/null +++ b/cem/events_test.go @@ -0,0 +1,20 @@ +package cem + +import ( + spineapi "github.com/enbility/spine-go/api" +) + +func (s *CemSuite) Test_Events() { + payload := spineapi.EventPayload{ + Device: s.mockRemoteDevice, + } + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDeviceChange + payload.ChangeType = spineapi.ElementChangeRemove + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDeviceChange + payload.ChangeType = spineapi.ElementChangeRemove + s.sut.HandleEvent(payload) +} diff --git a/cem/types.go b/cem/types.go new file mode 100644 index 0000000..1505ac8 --- /dev/null +++ b/cem/types.go @@ -0,0 +1,12 @@ +package cem + +import "github.com/enbility/cemd/api" + +const ( + + // A paired remote device was connected + DeviceConnected api.EventType = "deviceConnected" + + // A paired remote device was disconnected + DeviceDisconnected api.EventType = "deviceDisconnected" +) diff --git a/cmd/democem/democem.go b/cmd/democem/democem.go new file mode 100644 index 0000000..1e23ddb --- /dev/null +++ b/cmd/democem/democem.go @@ -0,0 +1,133 @@ +package democem + +import ( + "fmt" + "time" + + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/cem" + "github.com/enbility/cemd/ucevsecc" + "github.com/enbility/cemd/uclpcserver" + "github.com/enbility/cemd/uclppserver" + eebusapi "github.com/enbility/eebus-go/api" + "github.com/enbility/ship-go/logging" +) + +type DemoCem struct { + cem *cem.Cem + + remoteSki string +} + +func NewDemoCem(configuration *eebusapi.Configuration, remoteSki string) *DemoCem { + demo := &DemoCem{ + remoteSki: remoteSki, + } + + demo.cem = cem.NewCEM(configuration, demo, demo.deviceEventCB, demo) + + return demo +} + +func (d *DemoCem) Setup() error { + if err := d.cem.Setup(); err != nil { + return err + } + + lpcs := uclpcserver.NewUCLPC(d.cem.Service, d.entityEventCB) + d.cem.AddUseCase(lpcs) + + if err := lpcs.SetConsumptionLimit(api.LoadLimit{ + IsChangeable: true, + IsActive: false, + Value: 0, + }); err != nil { + logging.Log().Error(err) + } + if err := lpcs.SetContractualConsumptionNominalMax(22000); err != nil { + logging.Log().Error(err) + } + if err := lpcs.SetFailsafeConsumptionActivePowerLimit(4300, true); err != nil { + logging.Log().Error(err) + } + if err := lpcs.SetFailsafeDurationMinimum(time.Hour*2, true); err != nil { + logging.Log().Error(err) + } + + lpps := uclppserver.NewUCLPP(d.cem.Service, d.entityEventCB) + d.cem.AddUseCase(lpps) + + if err := lpps.SetProductionLimit(api.LoadLimit{ + IsChangeable: true, + IsActive: false, + Value: 0, + }); err != nil { + logging.Log().Error(err) + } + if err := lpps.SetContractualProductionNominalMax(-7000); err != nil { + logging.Log().Error(err) + } + if err := lpps.SetFailsafeProductionActivePowerLimit(0, true); err != nil { + logging.Log().Error(err) + } + if err := lpps.SetFailsafeDurationMinimum(time.Hour*2, true); err != nil { + logging.Log().Error(err) + } + + evsecc := ucevsecc.NewUCEVSECC(d.cem.Service, d.entityEventCB) + d.cem.AddUseCase(evsecc) + + d.cem.Service.RegisterRemoteSKI(d.remoteSki) + + d.cem.Start() + + return nil +} + +// Logging interface + +func (d *DemoCem) Trace(args ...interface{}) { + d.print("TRACE", args...) +} + +func (d *DemoCem) Tracef(format string, args ...interface{}) { + d.printFormat("TRACE", format, args...) +} + +func (d *DemoCem) Debug(args ...interface{}) { + d.print("DEBUG", args...) +} + +func (d *DemoCem) Debugf(format string, args ...interface{}) { + d.printFormat("DEBUG", format, args...) +} + +func (d *DemoCem) Info(args ...interface{}) { + d.print("INFO ", args...) +} + +func (d *DemoCem) Infof(format string, args ...interface{}) { + d.printFormat("INFO ", format, args...) +} + +func (d *DemoCem) Error(args ...interface{}) { + d.print("ERROR", args...) +} + +func (d *DemoCem) Errorf(format string, args ...interface{}) { + d.printFormat("ERROR", format, args...) +} + +func (d *DemoCem) currentTimestamp() string { + return time.Now().Format("2006-01-02 15:04:05") +} + +func (d *DemoCem) print(msgType string, args ...interface{}) { + value := fmt.Sprintln(args...) + fmt.Printf("%s %s %s", d.currentTimestamp(), msgType, value) +} + +func (d *DemoCem) printFormat(msgType, format string, args ...interface{}) { + value := fmt.Sprintf(format, args...) + fmt.Println(d.currentTimestamp(), msgType, value) +} diff --git a/cmd/democem/eventcb.go b/cmd/democem/eventcb.go new file mode 100644 index 0000000..5430272 --- /dev/null +++ b/cmd/democem/eventcb.go @@ -0,0 +1,13 @@ +package democem + +import ( + "github.com/enbility/cemd/api" + spineapi "github.com/enbility/spine-go/api" +) + +// Handle incoming usecase specific events +func (h *DemoCem) deviceEventCB(ski string, device spineapi.DeviceRemoteInterface, event api.EventType) { +} + +func (h *DemoCem) entityEventCB(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { +} diff --git a/cmd/democem/service.go b/cmd/democem/service.go new file mode 100644 index 0000000..2a0f7b7 --- /dev/null +++ b/cmd/democem/service.go @@ -0,0 +1,18 @@ +package democem + +import ( + eebusapi "github.com/enbility/eebus-go/api" + shipapi "github.com/enbility/ship-go/api" +) + +// report the Ship ID of a newly trusted connection +func (d *DemoCem) RemoteSKIConnected(service eebusapi.ServiceInterface, ski string) {} + +func (d *DemoCem) RemoteSKIDisconnected(service eebusapi.ServiceInterface, ski string) {} + +func (d *DemoCem) VisibleRemoteServicesUpdated(service eebusapi.ServiceInterface, entries []shipapi.RemoteService) { +} + +func (h *DemoCem) ServiceShipIDUpdate(ski string, shipdID string) {} + +func (h *DemoCem) ServicePairingDetailUpdate(ski string, detail *shipapi.ConnectionStateDetail) {} diff --git a/cmd/main.go b/cmd/main.go index 982a61e..c6d6550 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,153 +1,103 @@ package main import ( + "crypto/ecdsa" "crypto/tls" + "crypto/x509" + "encoding/pem" + "flag" "fmt" + "log" "os" "os/signal" - "strconv" "syscall" "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" + "github.com/enbility/cemd/cmd/democem" + eebusapi "github.com/enbility/eebus-go/api" + "github.com/enbility/ship-go/cert" + "github.com/enbility/ship-go/mdns" + "github.com/enbility/spine-go/model" ) -type DemoCem struct { - cem *cem.CemImpl -} - -func NewDemoCem(configuration *service.Configuration) *DemoCem { - demo := &DemoCem{} - - demo.cem = cem.NewCEM(configuration, demo, demo) - - return demo -} - -func (d *DemoCem) Setup() error { - 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 -func (d *DemoCem) RemoteServiceShipIDReported(service *service.EEBUSService, ski string, shipID string) { - // we should associated the Ship ID with the SKI and store it - // so the next connection can start trusted - logging.Log.Info("SKI", ski, "has Ship ID:", shipID) -} - -func (d *DemoCem) RemoteSKIConnected(service *service.EEBUSService, ski string) {} - -func (d *DemoCem) RemoteSKIDisconnected(service *service.EEBUSService, ski string) {} - -func (h *DemoCem) ReportServiceShipID(ski string, shipdID string) {} - -// Logging interface - -func (d *DemoCem) log(level string, args ...interface{}) { - t := time.Now() - fmt.Printf("%s: %s %s", t.Format(time.RFC3339), level, fmt.Sprintln(args...)) -} - -func (d *DemoCem) logf(level, format string, args ...interface{}) { - t := time.Now() - fmt.Printf("%s: %s %s\n", t.Format(time.RFC3339), level, fmt.Sprintf(format, args...)) -} - -func (d *DemoCem) Trace(args ...interface{}) { - d.log("TRACE", args...) -} - -func (d *DemoCem) Tracef(format string, args ...interface{}) { - d.logf("TRACE", format, args...) -} - -func (d *DemoCem) Debug(args ...interface{}) { - d.log("DEBUG", args...) -} - -func (d *DemoCem) Debugf(format string, args ...interface{}) { - d.logf("DEBUG", format, args...) -} - -func (d *DemoCem) Info(args ...interface{}) { - d.log("INFO", args...) -} - -func (d *DemoCem) Infof(format string, args ...interface{}) { - d.logf("INFO", format, args...) -} - -func (d *DemoCem) Error(args ...interface{}) { - d.log("ERROR", args...) -} - -func (d *DemoCem) Errorf(format string, args ...interface{}) { - d.logf("ERROR", format, args...) -} - // main app -func usage() { - fmt.Println("Usage: go run /cmd/main.go ") -} - func main() { - if len(os.Args) < 5 { - usage() - return - } + remoteSki := flag.String("remoteski", "", "The remote device SKI") + port := flag.Int("port", 4815, "Optional port for the EEBUS service") + crt := flag.String("crt", "cert.crt", "Optional filepath for the cert file") + key := flag.String("key", "cert.key", "Optional filepath for the key file") + iface := flag.String("iface", "", "Optional network interface the EEBUS connection should be limited to") - portValue, err := strconv.Atoi(os.Args[1]) - if err != nil { - fmt.Println("Port is invalid:", err) + flag.Parse() + + if len(os.Args) == 1 || remoteSki == nil || *remoteSki == "" { + flag.Usage() return } - certificate, err := tls.LoadX509KeyPair(os.Args[3], os.Args[4]) + certificate, err := tls.LoadX509KeyPair(*crt, *key) if err != nil { - fmt.Println("Certificate is invalid:", err) - return + certificate, err = cert.CreateCertificate("Demo", "Demo", "DE", "Demo-Unit-10") + if err != nil { + log.Fatal(err) + } + + // persist certificate into default files + pemdata := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: certificate.Certificate[0], + }) + err := os.WriteFile("cert.crt", pemdata, 0600) + if err != nil { + log.Fatal(err) + } + + b, err := x509.MarshalECPrivateKey(certificate.PrivateKey.(*ecdsa.PrivateKey)) + if err != nil { + log.Fatal(err) + } + pemdata = pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: b}) + err = os.WriteFile("cert.key", pemdata, 0600) + if err != nil { + log.Fatal(err) + } + } else { + fmt.Println("Using certificate file", *crt, "and key file", *key) } - ifaces := []string{os.Args[5]} - - configuration, err := service.NewConfiguration( + configuration, err := eebusapi.NewConfiguration( "Demo", "Demo", - "HEMS", + "Device", "123456789", model.DeviceTypeTypeEnergyManagementSystem, - portValue, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + *port, certificate, - 230) + 230, + time.Second*4) if err != nil { fmt.Println("Service data is invalid:", err) return } - configuration.SetInterfaces(ifaces) - demo := NewDemoCem(configuration) + configuration.SetMdnsProviderSelection(mdns.MdnsProviderSelectionGoZeroConfOnly) + + if iface != nil && *iface != "" { + ifaces := []string{*iface} + + configuration.SetInterfaces(ifaces) + } + + demo := democem.NewDemoCem(configuration, *remoteSki) + if err := demo.Setup(); err != nil { fmt.Println("Error setting up cem: ", err) return } - remoteService := service.NewServiceDetails(os.Args[2]) - demo.cem.RegisterEmobilityRemoteDevice(remoteService, nil) + // remoteService := shipapi.NewServiceDetails(*remoteSki) + // demo.emobilityScenario.RegisterRemoteDevice(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 deleted file mode 100644 index 07484f2..0000000 --- a/emobility/emobility.go +++ /dev/null @@ -1,265 +0,0 @@ -package emobility - -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 if an EV is connected - EVConnected() bool - - // return the current charge state of the EV - EVCurrentChargeState() (EVChargeStateType, error) - - // return the number of ac connected phases of the EV or 0 if it is unknown - EVConnectedPhases() (uint, error) - - // return the charged energy measurement in Wh of the connected EV - // - // possible errors: - // - ErrDataNotAvailable if no such measurement is (yet) available - // - and others - EVChargedEnergy() (float64, error) - - // return the last power measurement for each phase of the connected EV - // - // possible errors: - // - ErrDataNotAvailable if no such measurement is (yet) available - // - and others - EVPowerPerPhase() ([]float64, error) - - // return the last current measurement for each phase of the connected EV - // - // possible errors: - // - ErrDataNotAvailable if no such measurement is (yet) available - // - and others - EVCurrentsPerPhase() ([]float64, error) - - // return the min, max, default limits for each phase of the connected EV - // - // possible errors: - // - ErrDataNotAvailable if no such measurement is (yet) available - // - 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: - // - obligations: Overload Protection Limits per phase in A - // - recommendations: Self Consumption recommendations per phase in A - // - // obligations: - // Sets a maximum A limit for each phase that the EV may not exceed. - // Mainly used for implementing overload protection of the site or limiting the - // maximum charge power of EVs when the EV and EVSE communicate via IEC61851 - // and with ISO15118 if the EV does not support the Optimization of Self Consumption - // usecase. - // - // recommendations: - // Sets a recommended charge power in A for each phase. This is mainly - // used if the EV and EVSE communicate via ISO15118 to support charging excess solar power. - // The EV either needs to support the Optimization of Self Consumption usecase or - // 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. - EVWriteLoadControlLimits(obligations, recommendations []float64) error - - // return the current communication standard type used to communicate between EVSE and EV - // - // if an EV is connected via IEC61851, no ISO15118 specific data can be provided! - // sometimes the connection starts with IEC61851 before it switches - // to ISO15118, and sometimes it falls back again. so the error return is - // never absolut for the whole connection time, except if the use case - // is not supported - // - // the values are not constant and can change due to communication problems, bugs, and - // sometimes communication starts with IEC61851 before it switches to ISO - // - // possible errors: - // - ErrDataNotAvailable if that information is not (yet) available - // - ErrNotSupported if getting the communication standard is not supported - // - and others - EVCommunicationStandard() (EVCommunicationStandardType, error) - - // returns the identification of the currently connected EV or nil if not available - // - // possible errors: - // - ErrDataNotAvailable if that information is not (yet) available - // - and others - EVIdentification() (string, error) - - // returns if the EVSE and EV combination support optimzation of self consumption - // - // possible errors: - // - ErrDataNotAvailable if that information is not (yet) available - // - and others - EVOptimizationOfSelfConsumptionSupported() (bool, error) - - // return if the EVSE and EV combination support providing an SoC - // - // requires EVSoCSupported to return true - // only works with a current ISO15118-2 with VAS or ISO15118-20 - // communication between EVSE and EV - // - // possible errors: - // - ErrDataNotAvailable if no such measurement is (yet) available - // - and others - EVSoCSupported() (bool, error) - - // return the last known SoC of the connected EV - // - // requires EVSoCSupported to return true - // only works with a current ISO15118-2 with VAS or ISO15118-20 - // communication between EVSE and EV - // - // possible errors: - // - ErrNotSupported if support for SoC is not possible - // - ErrDataNotAvailable if no such measurement is (yet) available - // - and others - EVSoC() (float64, error) - - // returns if the EVSE and EV combination support coordinated charging - // - // possible errors: - // - 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 { - entity *spine.EntityLocalImpl - - service *service.EEBUSService - - evseEntity *spine.EntityRemoteImpl - evEntity *spine.EntityRemoteImpl - - evseDeviceClassification *features.DeviceClassification - evseDeviceDiagnosis *features.DeviceDiagnosis - - evDeviceClassification *features.DeviceClassification - evDeviceDiagnosis *features.DeviceDiagnosis - evDeviceConfiguration *features.DeviceConfiguration - evElectricalConnection *features.ElectricalConnection - evMeasurement *features.Measurement - evIdentification *features.Identification - evLoadControl *features.LoadControl - evTimeSeries *features.TimeSeries - evIncentiveTable *features.IncentiveTable - - evCurrentChargeStrategy EVChargeStrategyType - - ski string - currency model.CurrencyType - - configuration EmobilityConfiguration - dataProvider EmobilityDataProvider -} - -var _ EmobilityI = (*EMobilityImpl)(nil) - -// Add E-Mobility support -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, - currency: currency, - dataProvider: dataProvider, - evCurrentChargeStrategy: EVChargeStrategyTypeUnknown, - configuration: configuration, - } - spine.Events.Subscribe(emobility) - - service.PairRemoteService(details) - - return emobility -} diff --git a/emobility/evcoordinatedcharging_test.go b/emobility/evcoordinatedcharging_test.go deleted file mode 100644 index a2f8c0b..0000000 --- a/emobility/evcoordinatedcharging_test.go +++ /dev/null @@ -1,404 +0,0 @@ -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 deleted file mode 100644 index bd7ba69..0000000 --- a/emobility/events.go +++ /dev/null @@ -1,564 +0,0 @@ -package emobility - -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" - "github.com/enbility/eebus-go/util" -) - -// Internal EventHandler Interface for the CEM -func (e *EMobilityImpl) HandleEvent(payload spine.EventPayload) { - // only care about the registered SKI - if payload.Ski != e.ski { - return - } - - // only care about events for this remote device - if payload.Device != nil && payload.Device.Ski() != e.ski { - return - } - - // we care only about events from an EVSE or EV entity or device changes for this remote device - var entityType model.EntityTypeType - if payload.Entity != nil { - entityType = payload.Entity.EntityType() - if entityType != model.EntityTypeTypeEVSE && entityType != model.EntityTypeTypeEV { - return - } - } - - switch payload.EventType { - case spine.EventTypeDeviceChange: - switch payload.ChangeType { - case spine.ElementChangeRemove: - e.evseDisconnected() - e.evDisconnected() - } - - case spine.EventTypeEntityChange: - if payload.Entity == nil { - return - } - - switch payload.ChangeType { - case spine.ElementChangeAdd: - switch entityType { - case model.EntityTypeTypeEVSE: - e.evseConnected(payload.Ski, payload.Entity) - case model.EntityTypeTypeEV: - e.evConnected(payload.Entity) - } - case spine.ElementChangeRemove: - switch entityType { - case model.EntityTypeTypeEVSE: - e.evseDisconnected() - case model.EntityTypeTypeEV: - e.evDisconnected() - } - } - - case spine.EventTypeDataChange: - if payload.ChangeType == spine.ElementChangeUpdate { - switch payload.Data.(type) { - case *model.DeviceConfigurationKeyValueDescriptionListDataType: - if e.evDeviceConfiguration == nil { - break - } - - // key value descriptions received, now get the data - if _, err := e.evDeviceConfiguration.RequestKeyValues(); err != nil { - logging.Log.Error("Error getting configuration key values:", err) - } - - case *model.ElectricalConnectionParameterDescriptionListDataType: - if e.evElectricalConnection == nil { - break - } - if _, err := e.evElectricalConnection.RequestPermittedValueSets(); err != nil { - logging.Log.Error("Error getting electrical permitted values:", err) - } - - case *model.LoadControlLimitDescriptionListDataType: - if e.evLoadControl == nil { - break - } - if _, err := e.evLoadControl.RequestLimitValues(); err != nil { - logging.Log.Error("Error getting loadcontrol limit values:", err) - } - - 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.TimeSeriesDescriptionListDataType: - if e.evTimeSeries == nil || payload.CmdClassifier == nil { - break - } - - 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) - } - } - - case *model.TimeSeriesConstraintsListDataType: - if e.evTimeSeries == nil || payload.CmdClassifier == nil { - break - } - - if *payload.CmdClassifier != model.CmdClassifierTypeReply { - break - } - - e.evRequestTimeSeriesValues() - - case *model.TimeSeriesListDataType: - if e.evTimeSeries == nil || payload.CmdClassifier == nil { - break - } - - // check if we received a plan - e.evForwardChargePlanIfProvided() - - case *model.IncentiveDescriptionDataType: - if e.evIncentiveTable == nil || payload.CmdClassifier == nil { - break - } - - switch *payload.CmdClassifier { - case model.CmdClassifierTypeReply: - if err := e.evIncentiveTable.RequestConstraints(); err != nil { - break - } - - // if constraints do not exist, directly request values - e.evRequestIncentiveValues() - - 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 - } - - 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 -func (e *EMobilityImpl) evseConnected(ski string, entity *spine.EntityRemoteImpl) { - e.evseEntity = entity - localDevice := e.service.LocalDevice() - - f1, err := features.NewDeviceClassification(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) - if err != nil { - return - } - e.evseDeviceClassification = f1 - - f2, err := features.NewDeviceDiagnosis(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) - if err != nil { - return - } - e.evseDeviceDiagnosis = f2 - - _, _ = e.evseDeviceClassification.RequestManufacturerDetails() - _, _ = e.evseDeviceDiagnosis.RequestState() -} - -// an EV was disconnected -func (e *EMobilityImpl) evseDisconnected() { - e.evseEntity = nil - - e.evseDeviceClassification = nil - e.evseDeviceDiagnosis = nil - - e.evDisconnected() -} - -// an EV was disconnected, trigger required cleanup -func (e *EMobilityImpl) evDisconnected() { - if e.evEntity == nil { - return - } - - e.evEntity = nil - - e.evDeviceClassification = nil - e.evDeviceDiagnosis = nil - e.evDeviceConfiguration = nil - e.evElectricalConnection = nil - 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 -func (e *EMobilityImpl) evConnected(entity *spine.EntityRemoteImpl) { - e.evEntity = entity - localDevice := e.service.LocalDevice() - - logging.Log.Debug("ev connected") - - // setup features - e.evDeviceClassification, _ = features.NewDeviceClassification(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) - e.evDeviceDiagnosis, _ = features.NewDeviceDiagnosis(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) - e.evDeviceConfiguration, _ = features.NewDeviceConfiguration(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) - e.evElectricalConnection, _ = features.NewElectricalConnection(model.RoleTypeClient, model.RoleTypeServer, localDevice, entity) - 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) - } - if err := e.evDeviceConfiguration.SubscribeForEntity(); err != nil { - logging.Log.Debug(err) - } - if err := e.evDeviceDiagnosis.SubscribeForEntity(); err != nil { - logging.Log.Debug(err) - } - if err := e.evElectricalConnection.SubscribeForEntity(); err != nil { - logging.Log.Debug(err) - } - if err := e.evMeasurement.SubscribeForEntity(); err != nil { - logging.Log.Debug(err) - } - if err := e.evLoadControl.SubscribeForEntity(); err != nil { - logging.Log.Debug(err) - } - if err := e.evIdentification.SubscribeForEntity(); err != nil { - logging.Log.Debug(err) - } - - 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) - } - - 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.RequestDescriptions(); err != nil { - logging.Log.Debug(err) - } - - // get manufacturer details - if _, err := e.evDeviceClassification.RequestManufacturerDetails(); err != nil { - logging.Log.Debug(err) - } - - // get device diagnosis state - if _, err := e.evDeviceDiagnosis.RequestState(); err != nil { - logging.Log.Debug(err) - } - - // get electrical connection parameter - if err := e.evElectricalConnection.RequestDescriptions(); err != nil { - logging.Log.Debug(err) - } - - if err := e.evElectricalConnection.RequestParameterDescriptions(); err != nil { - logging.Log.Debug(err) - } - - // get measurement parameters - 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) - } - - // get identification - 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 - } - - 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 deleted file mode 100644 index 75ab384..0000000 --- a/emobility/events_evGetTimeSeriesPlanData_test.go +++ /dev/null @@ -1,107 +0,0 @@ -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 deleted file mode 100644 index 8e7c734..0000000 --- a/emobility/helper_test.go +++ /dev/null @@ -1,381 +0,0 @@ -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 deleted file mode 100644 index 25a9b22..0000000 --- a/emobility/mock_emobility.go +++ /dev/null @@ -1,415 +0,0 @@ -// 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)) -} - -// EVConnected mocks base method. -func (m *MockEmobilityI) EVConnected() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "EVConnected") - ret0, _ := ret[0].(bool) - return ret0 -} - -// EVConnected indicates an expected call of EVConnected. -func (mr *MockEmobilityIMockRecorder) EVConnected() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVConnected", reflect.TypeOf((*MockEmobilityI)(nil).EVConnected)) -} - -// 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 deleted file mode 100644 index bfded8f..0000000 --- a/emobility/public.go +++ /dev/null @@ -1,956 +0,0 @@ -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" -) - -// return if an EV is connected -// -// this includes all required features and -// minimal data being available -func (e *EMobilityImpl) EVConnected() bool { - // To report an EV as being connected, also consider all required - // features to be available and assigned - if e.evEntity == nil || - e.evDeviceDiagnosis == nil || - e.evElectricalConnection == nil || - e.evMeasurement == nil || - e.evLoadControl == nil || - e.evDeviceConfiguration == nil { - return false - } - - // getting current charge state should work - if _, err := e.EVCurrentChargeState(); err != nil { - return false - } - - // the communication standard needs to be available - if _, err := e.EVCommunicationStandard(); err != nil { - return false - } - - // getting currents measurements should work - if _, err := e.EVCurrentsPerPhase(); err != nil && err != features.ErrDataNotAvailable { - // features.ErrDataNotAvailable check in case of measurements not being provided but the feature works - return false - } - - // getting limits should work - if _, err := e.EVLoadControlObligationLimits(); err != nil && err != features.ErrDataNotAvailable { - // features.ErrDataNotAvailable check in case of load control limits not being provided but the feature works - return false - } - - return true -} - -// return the current charge state of the EV -func (e *EMobilityImpl) EVCurrentChargeState() (EVChargeStateType, error) { - if e.evEntity == nil || e.evDeviceDiagnosis == nil { - return EVChargeStateTypeUnplugged, nil - } - - diagnosisState, err := e.evDeviceDiagnosis.GetState() - if err != nil { - return EVChargeStateTypeUnknown, err - } - - operatingState := diagnosisState.OperatingState - if operatingState == nil { - return EVChargeStateTypeUnknown, features.ErrDataNotAvailable - } - - switch *operatingState { - case model.DeviceDiagnosisOperatingStateTypeNormalOperation: - return EVChargeStateTypeActive, nil - case model.DeviceDiagnosisOperatingStateTypeStandby: - return EVChargeStateTypePaused, nil - case model.DeviceDiagnosisOperatingStateTypeFailure: - return EVChargeStateTypeError, nil - case model.DeviceDiagnosisOperatingStateTypeFinished: - return EVChargeStateTypeFinished, nil - } - - return EVChargeStateTypeUnknown, nil -} - -// return the number of ac connected phases of the EV or 0 if it is unknown -func (e *EMobilityImpl) EVConnectedPhases() (uint, error) { - if e.evEntity == nil || e.evElectricalConnection == nil { - return 0, ErrEVDisconnected - } - - 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 -// -// possible errors: -// - ErrDataNotAvailable if no such measurement is (yet) available -// - and others -func (e *EMobilityImpl) EVChargedEnergy() (float64, error) { - if e.evEntity == nil || e.evMeasurement == nil { - return 0, ErrEVDisconnected - } - - 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 value.GetValue(), err -} - -// return the last power measurement for each phase of the connected EV -// -// possible errors: -// - ErrDataNotAvailable if no such measurement is (yet) available -// - and others -func (e *EMobilityImpl) EVPowerPerPhase() ([]float64, error) { - if e.evEntity == nil || e.evMeasurement == nil { - return nil, ErrEVDisconnected - } - - var data []model.MeasurementDataType - - 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 - } - } - - var result []float64 - - 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) - } - } - - return result, nil -} - -// return the last current measurement for each phase of the connected EV -// -// possible errors: -// - ErrDataNotAvailable if no such measurement is (yet) available -// - and others -func (e *EMobilityImpl) EVCurrentsPerPhase() ([]float64, error) { - if e.evEntity == nil || e.evElectricalConnection == nil { - return nil, ErrEVDisconnected - } - - 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 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) - } - } - - return result, nil -} - -// return the min, max, default limits for each phase of the connected EV -// -// possible errors: -// - ErrDataNotAvailable if no such measurement is (yet) available -// - and others -func (e *EMobilityImpl) EVCurrentLimits() ([]float64, []float64, []float64, error) { - if e.evEntity == nil || e.evElectricalConnection == nil { - return nil, nil, nil, ErrEVDisconnected - } - - 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 || e.evElectricalConnection == nil || e.evLoadControl == nil { - return nil, ErrEVDisconnected - } - - // 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, features.ErrDataNotAvailable - } - - var result []float64 - - for i := 0; i < 3; i++ { - phaseName := util.PhaseNameMapping[i] - - // 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 - } - - var limitDesc *model.LoadControlLimitDescriptionDataType - for _, desc := range limitDescriptions { - if desc.MeasurementId != nil && *desc.MeasurementId == *elParamDesc.MeasurementId { - limitDesc = &desc - break - } - } - - if limitDesc == nil || limitDesc.LimitId == nil { - return nil, features.ErrDataNotAvailable - } - - limitIdData, err := e.evLoadControl.GetLimitValueForLimitId(*limitDesc.LimitId) - if err != nil { - return nil, features.ErrDataNotAvailable - } - - var limitValue float64 - if limitIdData.Value == nil { - // assume maximum possible - _, dataMax, _, err := e.evElectricalConnection.GetLimitsForParameterId(*elParamDesc.ParameterId) - if err != nil { - return nil, features.ErrDataNotAvailable - } - - limitValue = dataMax - } else { - limitValue = limitIdData.Value.GetValue() - } - - result = append(result, limitValue) - } - - return result, nil -} - -// send new LoadControlLimits to the remote EV -// -// parameters: -// - obligations: Overload Protection Limits per phase in A -// - recommendations: Self Consumption recommendations per phase in A -// -// obligations: -// Sets a maximum A limit for each phase that the EV may not exceed. -// Mainly used for implementing overload protection of the site or limiting the -// maximum charge power of EVs when the EV and EVSE communicate via IEC61851 -// and with ISO15118 if the EV does not support the Optimization of Self Consumption -// usecase. -// -// recommendations: -// Sets a recommended charge power in A for each phase. This is mainly -// used if the EV and EVSE communicate via ISO15118 to support charging excess solar power. -// The EV either needs to support the Optimization of Self Consumption usecase or -// the EVSE needs to be able map the recommendations into oligation limits which then -// works for all EVs communication either via IEC61851 or ISO15118. -// -// 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 - } - - if e.evElectricalConnection == nil || e.evLoadControl == nil { - return features.ErrDataNotAvailable - } - - var limitData []model.LoadControlLimitDataType - - for scopeTypes := 0; scopeTypes < 2; scopeTypes++ { - category := model.LoadControlCategoryTypeObligation - currentsPerPhase := obligations - if scopeTypes == 1 { - category = model.LoadControlCategoryTypeRecommendation - currentsPerPhase = recommendations - } - - for index, phaseLimit := range currentsPerPhase { - phaseName := util.PhaseNameMapping[index] - - // 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 - } - - // electricalParameterDescription contains the measured phase for each measurementId - elParamDesc, err := e.evElectricalConnection.GetParameterDescriptionForMeasuredPhase(phaseName) - if err != nil || elParamDesc.MeasurementId == nil { - continue - } - - var limitDesc *model.LoadControlLimitDescriptionDataType - for _, desc := range limitDescriptions { - if desc.MeasurementId != nil && *desc.MeasurementId == *elParamDesc.MeasurementId { - limitDesc = &desc - break - } - } - - if limitDesc == nil || limitDesc.LimitId == nil { - continue - } - - limitIdData, err := e.evLoadControl.GetLimitValueForLimitId(*limitDesc.LimitId) - if err != nil { - continue - } - - // 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 - } - - // electricalPermittedValueSet contains the allowed min, max and the default values per phase - phaseLimit = e.evElectricalConnection.AdjustValueToBeWithinPermittedValuesForParameter(phaseLimit, *elParamDesc.ParameterId) - - newLimit := model.LoadControlLimitDataType{ - LimitId: limitDesc.LimitId, - IsLimitActive: eebusUtil.Ptr(true), - Value: model.NewScaledNumberType(phaseLimit), - } - limitData = append(limitData, newLimit) - } - } - - _, err := e.evLoadControl.WriteLimitValues(limitData) - - return err -} - -// return the current communication standard type used to communicate between EVSE and EV -// -// if an EV is connected via IEC61851, no ISO15118 specific data can be provided! -// sometimes the connection starts with IEC61851 before it switches -// to ISO15118, and sometimes it falls back again. so the error return is -// never absolut for the whole connection time, except if the use case -// is not supported -// -// the values are not constant and can change due to communication problems, bugs, and -// sometimes communication starts with IEC61851 before it switches to ISO -// -// possible errors: -// - ErrDataNotAvailable if that information is not (yet) available -// - ErrNotSupported if getting the communication standard is not supported -// - and others -func (e *EMobilityImpl) EVCommunicationStandard() (EVCommunicationStandardType, error) { - if e.evEntity == nil || e.evDeviceConfiguration == nil { - return EVCommunicationStandardTypeUnknown, ErrEVDisconnected - } - - // check if device configuration descriptions has an communication standard key name - _, err := e.evDeviceConfiguration.GetDescriptionForKeyName(model.DeviceConfigurationKeyNameTypeCommunicationsStandard) - if err != nil { - return EVCommunicationStandardTypeUnknown, err - } - - data, err := e.evDeviceConfiguration.GetKeyValueForKeyName(model.DeviceConfigurationKeyNameTypeCommunicationsStandard, model.DeviceConfigurationKeyValueTypeTypeString) - if err != nil { - return EVCommunicationStandardTypeUnknown, 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 -// -// possible errors: -// - ErrDataNotAvailable if that information is not (yet) available -// - and others -func (e *EMobilityImpl) EVIdentification() (string, error) { - if e.evEntity == nil { - return "", ErrEVDisconnected - } - - if e.evIdentification == nil { - return "", features.ErrDataNotAvailable - } - - identifications, err := e.evIdentification.GetValues() - if err != nil { - return "", err - } - - for _, identification := range identifications { - value := identification.IdentificationValue - if value == nil { - continue - } - - return string(*value), nil - } - return "", nil -} - -// returns if the EVSE and EV combination support optimzation of self consumption -// -// possible errors: -// - ErrDataNotAvailable if that information is not (yet) available -// - and others -func (e *EMobilityImpl) EVOptimizationOfSelfConsumptionSupported() (bool, error) { - if e.evEntity == nil || e.evLoadControl == nil { - return false, ErrEVDisconnected - } - - evEntity, err := util.EntityOfTypeForSki(e.service, model.EntityTypeTypeEV, e.ski) - if err != nil { - return false, err - } - - // check if the Optimization of self consumption usecase is supported - if !util.IsUsecaseSupported(model.UseCaseNameTypeOptimizationOfSelfConsumptionDuringEVCharging, model.UseCaseActorTypeEV, evEntity.Device()) { - return false, nil - } - - // check if loadcontrol limit descriptions contains a recommendation category - if _, err = e.evLoadControl.GetLimitDescriptionsForCategory(model.LoadControlCategoryTypeRecommendation); err != nil { - return false, err - } - - return true, nil -} - -// return if the EVSE and EV combination support providing an SoC -// -// requires EVSoCSupported to return true -// only works with a current ISO15118-2 with VAS or ISO15118-20 -// communication between EVSE and EV -// -// possible errors: -// - ErrDataNotAvailable if no such measurement is (yet) available -// - and others -func (e *EMobilityImpl) EVSoCSupported() (bool, error) { - if e.evEntity == nil || e.evMeasurement == nil { - return false, ErrEVDisconnected - } - - evEntity, err := util.EntityOfTypeForSki(e.service, model.EntityTypeTypeEV, e.ski) - if err != nil { - return false, err - } - - // check if the SoC usecase is supported - if !util.IsUsecaseSupported(model.UseCaseNameTypeEVStateOfCharge, model.UseCaseActorTypeEV, evEntity.Device()) { - return false, nil - } - - // check if measurement descriptions has an SoC scope type - desc, err := e.evMeasurement.GetDescriptionsForScope(model.ScopeTypeTypeStateOfCharge) - if err != nil { - return false, err - } - if len(desc) == 0 { - return false, features.ErrDataNotAvailable - } - - return true, nil -} - -// return the last known SoC of the connected EV -// -// requires EVSoCSupported to return true -// only works with a current ISO15118-2 with VAS or ISO15118-20 -// communication between EVSE and EV -// -// possible errors: -// - ErrNotSupported if support for SoC is not possible -// - ErrDataNotAvailable if no such measurement is (yet) available -// - and others -func (e *EMobilityImpl) EVSoC() (float64, error) { - if e.evEntity == nil || e.evMeasurement == nil { - return 0, ErrEVDisconnected - } - - // check if the SoC is supported - support, err := e.EVSoCSupported() - if err != nil { - return 0, err - } - if !support { - return 0, features.ErrNotSupported - } - - data, err := e.evMeasurement.GetValuesForTypeCommodityScope(model.MeasurementTypeTypePercentage, model.CommodityTypeTypeElectricity, model.ScopeTypeTypeStateOfCharge) - if err != nil { - return 0, err - } - - // 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 -// -// possible errors: -// - ErrDataNotAvailable if that information is not (yet) available -// - and others -func (e *EMobilityImpl) EVCoordinatedChargingSupported() (bool, error) { - if e.evEntity == nil { - return false, ErrEVDisconnected - } - - evEntity, err := util.EntityOfTypeForSki(e.service, model.EntityTypeTypeEV, e.ski) - if err != nil { - return false, err - } - - // check if the Coordinated charging usecase is supported - if !util.IsUsecaseSupported(model.UseCaseNameTypeCoordinatedEVCharging, model.UseCaseActorTypeEV, evEntity.Device()) { - return false, nil - } - - 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.evEntity == nil || 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.evEntity == nil || 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.evEntity == nil || 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.evEntity == nil || 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 deleted file mode 100644 index 1602ef0..0000000 --- a/emobility/public_EVChargeStrategy_test.go +++ /dev/null @@ -1,199 +0,0 @@ -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 deleted file mode 100644 index dfede96..0000000 --- a/emobility/public_EVChargedEnergy_test.go +++ /dev/null @@ -1,72 +0,0 @@ -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 deleted file mode 100644 index 9bfdf1b..0000000 --- a/emobility/public_EVCommunicationStandard_test.go +++ /dev/null @@ -1,89 +0,0 @@ -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 deleted file mode 100644 index 1f24f80..0000000 --- a/emobility/public_EVConnectedPhases_test.go +++ /dev/null @@ -1,68 +0,0 @@ -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 deleted file mode 100644 index cd8b1a4..0000000 --- a/emobility/public_EVCoordinatedChargingSupported_test.go +++ /dev/null @@ -1,51 +0,0 @@ -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 deleted file mode 100644 index 7fa723c..0000000 --- a/emobility/public_EVCurrentChargeState_test.go +++ /dev/null @@ -1,98 +0,0 @@ -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.Nil(t, err) - assert.Equal(t, EVChargeStateTypeUnplugged, 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 deleted file mode 100644 index 7dfc77b..0000000 --- a/emobility/public_EVCurrentLimits_test.go +++ /dev/null @@ -1,179 +0,0 @@ -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 deleted file mode 100644 index eeeb407..0000000 --- a/emobility/public_EVCurrentsPerPhase_test.go +++ /dev/null @@ -1,97 +0,0 @@ -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 deleted file mode 100644 index 82b6af8..0000000 --- a/emobility/public_EVEnergyDemand_test.go +++ /dev/null @@ -1,236 +0,0 @@ -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 deleted file mode 100644 index 07f3f0b..0000000 --- a/emobility/public_EVGetIncentiveConstraints_test.go +++ /dev/null @@ -1,75 +0,0 @@ -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 deleted file mode 100644 index 8fe7b26..0000000 --- a/emobility/public_EVGetPowerConstraints_test.go +++ /dev/null @@ -1,69 +0,0 @@ -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 deleted file mode 100644 index a2ea394..0000000 --- a/emobility/public_EVIdentification_test.go +++ /dev/null @@ -1,52 +0,0 @@ -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 deleted file mode 100644 index 3370d39..0000000 --- a/emobility/public_EVOptimizationOfSelfConsumptionSupported_test.go +++ /dev/null @@ -1,77 +0,0 @@ -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 deleted file mode 100644 index ef1f7c8..0000000 --- a/emobility/public_EVPowerPerPhase_test.go +++ /dev/null @@ -1,185 +0,0 @@ -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 deleted file mode 100644 index d4a1ad8..0000000 --- a/emobility/public_EVSoCSupported_test.go +++ /dev/null @@ -1,77 +0,0 @@ -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 deleted file mode 100644 index dee20db..0000000 --- a/emobility/public_EVSoC_test.go +++ /dev/null @@ -1,118 +0,0 @@ -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 deleted file mode 100644 index 69d171e..0000000 --- a/emobility/public_EVWriteIncentives_test.go +++ /dev/null @@ -1,157 +0,0 @@ -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 deleted file mode 100644 index e7dcdc1..0000000 --- a/emobility/public_EVWriteLoadControlLimits_test.go +++ /dev/null @@ -1,255 +0,0 @@ -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 deleted file mode 100644 index 06a8c30..0000000 --- a/emobility/public_EVWritePowerLimits_test.go +++ /dev/null @@ -1,154 +0,0 @@ -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/results.go b/emobility/results.go deleted file mode 100644 index 3a7d6ca..0000000 --- a/emobility/results.go +++ /dev/null @@ -1,39 +0,0 @@ -package emobility - -import ( - "github.com/enbility/eebus-go/spine" - "github.com/enbility/eebus-go/spine/model" -) - -func (e *EMobilityImpl) HandleResult(errorMsg spine.ResultMessage) { - if errorMsg.EntityRemote == e.evseEntity { - // handle errors coming from the remote EVSE entity - switch errorMsg.FeatureLocal.Type() { - case model.FeatureTypeTypeDeviceDiagnosis: - e.handleResultDeviceDiagnosis(errorMsg) - } - - } else if e.evEntity != nil && errorMsg.EntityRemote == e.evEntity { - // handle errors coming from a remote EV entity - switch errorMsg.FeatureLocal.Type() { - case model.FeatureTypeTypeDeviceDiagnosis: - e.handleResultDeviceDiagnosis(errorMsg) - } - - } -} - -// Handle DeviceDiagnosis Results -func (e *EMobilityImpl) handleResultDeviceDiagnosis(resultMsg spine.ResultMessage) { - // is this an error for a heartbeat message? - if *resultMsg.Result.ErrorNumber == model.ErrorNumberTypeNoError { - return - } - - if resultMsg.DeviceRemote.IsHeartbeatMsgCounter(resultMsg.MsgCounterReference) { - resultMsg.DeviceRemote.Stopheartbeat() - - // something is horribly wrong, disconnect and hope a new connection will fix it - e.service.DisconnectSKI(resultMsg.DeviceRemote.Ski(), string(*resultMsg.Result.Description)) - } -} diff --git a/emobility/scenario.go b/emobility/scenario.go deleted file mode 100644 index b3f1e85..0000000 --- a/emobility/scenario.go +++ /dev/null @@ -1,167 +0,0 @@ -package emobility - -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" - "github.com/enbility/eebus-go/util" -) - -type EmobilityScenarioImpl struct { - *scenarios.ScenarioImpl - - remoteDevices map[string]*EMobilityImpl - - mux sync.Mutex - - currency model.CurrencyType - configuration EmobilityConfiguration -} - -var _ scenarios.ScenariosI = (*EmobilityScenarioImpl)(nil) - -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, - } -} - -// adds all the supported features to the local entity -func (e *EmobilityScenarioImpl) AddFeatures() { - localEntity := e.Service.LocalEntity() - - // server features - { - f := localEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) - f.AddResultHandler(e) - f.AddFunctionType(model.FunctionTypeDeviceDiagnosisStateData, true, false) - - // Set the initial state - deviceDiagnosisStateDate := &model.DeviceDiagnosisStateDataType{ - OperatingState: util.Ptr(model.DeviceDiagnosisOperatingStateTypeNormalOperation), - } - f.SetData(model.FunctionTypeDeviceDiagnosisStateData, deviceDiagnosisStateDate) - - f.AddFunctionType(model.FunctionTypeDeviceDiagnosisHeartbeatData, true, false) - } - - // client features - var clientFeatures = []model.FeatureTypeType{ - model.FeatureTypeTypeDeviceDiagnosis, - model.FeatureTypeTypeDeviceClassification, - model.FeatureTypeTypeDeviceConfiguration, - model.FeatureTypeTypeElectricalConnection, - model.FeatureTypeTypeMeasurement, - model.FeatureTypeTypeLoadControl, - model.FeatureTypeTypeIdentification, - } - - if e.configuration.CoordinatedChargingEnabled { - clientFeatures = append(clientFeatures, model.FeatureTypeTypeTimeSeries) - clientFeatures = append(clientFeatures, model.FeatureTypeTypeIncentiveTable) - } - for _, feature := range clientFeatures { - f := localEntity.GetOrAddFeature(feature, model.RoleTypeClient) - f.AddResultHandler(e) - } -} - -// add supported e-mobility usecases -func (e *EmobilityScenarioImpl) AddUseCases() { - localEntity := e.Service.LocalEntity() - - _ = spine.NewUseCase( - localEntity, - model.UseCaseNameTypeEVSECommissioningAndConfiguration, - model.SpecificationVersionType("1.0.1"), - []model.UseCaseScenarioSupportType{1, 2}) - - _ = spine.NewUseCase( - localEntity, - model.UseCaseNameTypeEVCommissioningAndConfiguration, - model.SpecificationVersionType("1.0.1"), - []model.UseCaseScenarioSupportType{1, 2, 3, 4, 5, 6, 7, 8}) - - _ = spine.NewUseCase( - localEntity, - model.UseCaseNameTypeMeasurementOfElectricityDuringEVCharging, - model.SpecificationVersionType("1.0.1"), - []model.UseCaseScenarioSupportType{1, 2, 3}) - - _ = spine.NewUseCase( - localEntity, - model.UseCaseNameTypeOverloadProtectionByEVChargingCurrentCurtailment, - model.SpecificationVersionType("1.0.1b"), - []model.UseCaseScenarioSupportType{1, 2, 3}) - - _ = spine.NewUseCaseWithActor( - localEntity, - model.UseCaseActorTypeMonitoringAppliance, - model.UseCaseNameTypeEVStateOfCharge, - model.SpecificationVersionType("1.0.0"), - []model.UseCaseScenarioSupportType{1, 2, 3, 4}) - - _ = spine.NewUseCase( - localEntity, - model.UseCaseNameTypeOptimizationOfSelfConsumptionDuringEVCharging, - model.SpecificationVersionType("1.0.1b"), - []model.UseCaseScenarioSupportType{1, 2, 3}) - - 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) 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() - defer e.mux.Unlock() - - if em, ok := e.remoteDevices[details.SKI()]; ok { - return em - } - - 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) UnRegisterRemoteDevice(remoteDeviceSki string) error { - e.mux.Lock() - defer e.mux.Unlock() - - delete(e.remoteDevices, remoteDeviceSki) - - return e.Service.UnpairRemoteService(remoteDeviceSki) -} - -func (e *EmobilityScenarioImpl) 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/emobility/types.go b/emobility/types.go deleted file mode 100644 index 40bd877..0000000 --- a/emobility/types.go +++ /dev/null @@ -1,81 +0,0 @@ -package emobility - -import ( - "errors" - "time" - - "github.com/enbility/eebus-go/spine/model" -) - -type EVCommunicationStandardType model.DeviceConfigurationKeyValueStringType - -const ( - EVCommunicationStandardTypeUnknown EVCommunicationStandardType = "unknown" - EVCommunicationStandardTypeISO151182ED1 EVCommunicationStandardType = "iso15118-2ed1" - EVCommunicationStandardTypeISO151182ED2 EVCommunicationStandardType = "iso15118-2ed2" - EVCommunicationStandardTypeIEC61851 EVCommunicationStandardType = "iec61851" -) - -type EVChargeStateType string - -const ( - EVChargeStateTypeUnknown EVChargeStateType = "Unknown" - EVChargeStateTypeUnplugged EVChargeStateType = "unplugged" - EVChargeStateTypeError EVChargeStateType = "error" - EVChargeStateTypePaused EVChargeStateType = "paused" - EVChargeStateTypeActive EVChargeStateType = "active" - EVChargeStateTypeFinished EVChargeStateType = "finished" -) - -type EVChargeStrategyType string - -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 138a14a..524bb35 100644 --- a/go.mod +++ b/go.mod @@ -1,29 +1,32 @@ module github.com/enbility/cemd -go 1.18 +go 1.21.1 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 + github.com/enbility/eebus-go v0.5.0 + github.com/enbility/ship-go v0.5.0 + github.com/enbility/spine-go v0.5.0 + github.com/stretchr/testify v1.8.4 ) require ( github.com/ahmetb/go-linq/v3 v3.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/enbility/zeroconf/v2 v2.0.0-20240210101930-d0004078577b // 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/gorilla/websocket v1.5.1 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/holoplot/go-avahi v0.0.0-20240210093433-b8dc0fc11e7e // indirect + github.com/miekg/dns v1.1.58 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rickb777/date v1.20.1 // indirect + github.com/rickb777/date v1.20.5 // indirect github.com/rickb777/plural v1.4.1 // indirect + github.com/stretchr/objx v0.5.1 // indirect gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect - golang.org/x/mod v0.9.0 // indirect - golang.org/x/net v0.8.0 // indirect - golang.org/x/sys v0.6.0 // indirect - golang.org/x/tools v0.7.0 // indirect + go.uber.org/mock v0.4.0 // indirect + golang.org/x/mod v0.15.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/tools v0.17.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 8006c4c..64ecafa 100644 --- a/go.sum +++ b/go.sum @@ -3,81 +3,60 @@ github.com/ahmetb/go-linq/v3 v3.2.0/go.mod h1:haQ3JfOeWK8HpVxMtHHEMPVgBKiYyQ+f1/ 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/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/enbility/eebus-go v0.5.0 h1:iC+CSc7eVGqls0GT4d4eWA0vrm1m9eMroG9rvEia06Y= +github.com/enbility/eebus-go v0.5.0/go.mod h1:JhLSoVxGiKSgOtxoGkA81vs+JRB4QHTa8P8LzHsq5WQ= +github.com/enbility/ship-go v0.5.0 h1:Uqol2XjzDOcvT8HUAE4B/59yqd3mxhpJJ/Q2eDHNGqc= +github.com/enbility/ship-go v0.5.0/go.mod h1:ovyrJE3oPnGT5+eQnOqWut80gFDQ0XHn3ZWU2fHV9xQ= +github.com/enbility/spine-go v0.5.0 h1:3OQBl8gQPW/iuWmwcabmCIXDcFCP0RsDw7uP8BYUmaY= +github.com/enbility/spine-go v0.5.0/go.mod h1:8rXOJ7nTa4qrSRK0PpfavBXMztxi6l+h/IFpIVmHviM= +github.com/enbility/zeroconf/v2 v2.0.0-20240210101930-d0004078577b h1:sg3c6LJ4eWffwtt9SW0lgcIX4Oh274vwdJnNFNNrDco= +github.com/enbility/zeroconf/v2 v2.0.0-20240210101930-d0004078577b/go.mod h1:BjzRRiYX6mWdOgku1xxDE+NsV8PijTby7Q7BkYVdfDU= 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= -github.com/holoplot/go-avahi v1.0.1 h1:XcqR2keL4qWRnlxHD5CAOdWpLFZJ+EOUK0vEuylfvvk= -github.com/holoplot/go-avahi v1.0.1/go.mod h1:qH5psEKb0DK+BRplMfc+RY4VMOlbf6mqfxgpMy6aP0M= -github.com/libp2p/zeroconf/v2 v2.2.0 h1:Cup06Jv6u81HLhIj1KasuNM/RHHrJ8T7wOTS4+Tv53Q= -github.com/libp2p/zeroconf/v2 v2.2.0/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs= -github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= -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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/holoplot/go-avahi v0.0.0-20240210093433-b8dc0fc11e7e h1:XOKmPp6CgtFByseoBaL5Ew9b6NWSie+nr6pMFeO0Tvc= +github.com/holoplot/go-avahi v0.0.0-20240210093433-b8dc0fc11e7e/go.mod h1:WRfsMEGa+MvsfqqKmS7Ye1jrnfRW6kfF/CTP9UMZj0Q= +github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= +github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= 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/date v1.20.5 h1:Ybjz7J7ga9ui4VJizQpil0l330r6wkn6CicaoattIxQ= +github.com/rickb777/date v1.20.5/go.mod h1:6BPrm3/aQI0I8jvlD1fAlm/86k5eSeTQ2mR5FEmTnSw= 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/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= +github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= 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= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= 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= diff --git a/grid/events.go b/grid/events.go deleted file mode 100644 index e263de6..0000000 --- a/grid/events.go +++ /dev/null @@ -1,139 +0,0 @@ -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 deleted file mode 100644 index 4b6d985..0000000 --- a/grid/grid.go +++ /dev/null @@ -1,50 +0,0 @@ -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 deleted file mode 100644 index 31d9124..0000000 --- a/grid/public.go +++ /dev/null @@ -1,255 +0,0 @@ -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 deleted file mode 100644 index 0d6a6fa..0000000 --- a/grid/results.go +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 8e1700d..0000000 --- a/grid/scenario.go +++ /dev/null @@ -1,94 +0,0 @@ -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 deleted file mode 100644 index e43a02e..0000000 --- a/inverterbatteryvis/events.go +++ /dev/null @@ -1,131 +0,0 @@ -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 deleted file mode 100644 index d5aa5ae..0000000 --- a/inverterbatteryvis/invertervis.go +++ /dev/null @@ -1,45 +0,0 @@ -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/results.go b/inverterbatteryvis/results.go deleted file mode 100644 index 14eb3bb..0000000 --- a/inverterbatteryvis/results.go +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index f52c9fa..0000000 --- a/inverterbatteryvis/scenario.go +++ /dev/null @@ -1,95 +0,0 @@ -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 deleted file mode 100644 index a069efa..0000000 --- a/inverterpvvis/events.go +++ /dev/null @@ -1,156 +0,0 @@ -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 deleted file mode 100644 index 3706db0..0000000 --- a/inverterpvvis/invertervis.go +++ /dev/null @@ -1,45 +0,0 @@ -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 deleted file mode 100644 index 2db1d5c..0000000 --- a/inverterpvvis/public.go +++ /dev/null @@ -1,106 +0,0 @@ -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 deleted file mode 100644 index 1d02bbd..0000000 --- a/inverterpvvis/results.go +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index b6a3271..0000000 --- a/inverterpvvis/scenario.go +++ /dev/null @@ -1,95 +0,0 @@ -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/mocks/CemInterface.go b/mocks/CemInterface.go new file mode 100644 index 0000000..156df30 --- /dev/null +++ b/mocks/CemInterface.go @@ -0,0 +1,177 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + api "github.com/enbility/cemd/api" + mock "github.com/stretchr/testify/mock" +) + +// CemInterface is an autogenerated mock type for the CemInterface type +type CemInterface struct { + mock.Mock +} + +type CemInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *CemInterface) EXPECT() *CemInterface_Expecter { + return &CemInterface_Expecter{mock: &_m.Mock} +} + +// AddUseCase provides a mock function with given fields: usecase +func (_m *CemInterface) AddUseCase(usecase api.UseCaseInterface) { + _m.Called(usecase) +} + +// CemInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type CemInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +// - usecase api.UseCaseInterface +func (_e *CemInterface_Expecter) AddUseCase(usecase interface{}) *CemInterface_AddUseCase_Call { + return &CemInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase", usecase)} +} + +func (_c *CemInterface_AddUseCase_Call) Run(run func(usecase api.UseCaseInterface)) *CemInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.UseCaseInterface)) + }) + return _c +} + +func (_c *CemInterface_AddUseCase_Call) Return() *CemInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *CemInterface_AddUseCase_Call) RunAndReturn(run func(api.UseCaseInterface)) *CemInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// Setup provides a mock function with given fields: +func (_m *CemInterface) Setup() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Setup") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CemInterface_Setup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Setup' +type CemInterface_Setup_Call struct { + *mock.Call +} + +// Setup is a helper method to define mock.On call +func (_e *CemInterface_Expecter) Setup() *CemInterface_Setup_Call { + return &CemInterface_Setup_Call{Call: _e.mock.On("Setup")} +} + +func (_c *CemInterface_Setup_Call) Run(run func()) *CemInterface_Setup_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CemInterface_Setup_Call) Return(_a0 error) *CemInterface_Setup_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CemInterface_Setup_Call) RunAndReturn(run func() error) *CemInterface_Setup_Call { + _c.Call.Return(run) + return _c +} + +// Shutdown provides a mock function with given fields: +func (_m *CemInterface) Shutdown() { + _m.Called() +} + +// CemInterface_Shutdown_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Shutdown' +type CemInterface_Shutdown_Call struct { + *mock.Call +} + +// Shutdown is a helper method to define mock.On call +func (_e *CemInterface_Expecter) Shutdown() *CemInterface_Shutdown_Call { + return &CemInterface_Shutdown_Call{Call: _e.mock.On("Shutdown")} +} + +func (_c *CemInterface_Shutdown_Call) Run(run func()) *CemInterface_Shutdown_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CemInterface_Shutdown_Call) Return() *CemInterface_Shutdown_Call { + _c.Call.Return() + return _c +} + +func (_c *CemInterface_Shutdown_Call) RunAndReturn(run func()) *CemInterface_Shutdown_Call { + _c.Call.Return(run) + return _c +} + +// Start provides a mock function with given fields: +func (_m *CemInterface) Start() { + _m.Called() +} + +// CemInterface_Start_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Start' +type CemInterface_Start_Call struct { + *mock.Call +} + +// Start is a helper method to define mock.On call +func (_e *CemInterface_Expecter) Start() *CemInterface_Start_Call { + return &CemInterface_Start_Call{Call: _e.mock.On("Start")} +} + +func (_c *CemInterface_Start_Call) Run(run func()) *CemInterface_Start_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CemInterface_Start_Call) Return() *CemInterface_Start_Call { + _c.Call.Return() + return _c +} + +func (_c *CemInterface_Start_Call) RunAndReturn(run func()) *CemInterface_Start_Call { + _c.Call.Return(run) + return _c +} + +// NewCemInterface creates a new instance of CemInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewCemInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *CemInterface { + mock := &CemInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/DeviceEventCallback.go b/mocks/DeviceEventCallback.go new file mode 100644 index 0000000..ad11a90 --- /dev/null +++ b/mocks/DeviceEventCallback.go @@ -0,0 +1,72 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + cemdapi "github.com/enbility/cemd/api" + api "github.com/enbility/spine-go/api" + + mock "github.com/stretchr/testify/mock" +) + +// DeviceEventCallback is an autogenerated mock type for the DeviceEventCallback type +type DeviceEventCallback struct { + mock.Mock +} + +type DeviceEventCallback_Expecter struct { + mock *mock.Mock +} + +func (_m *DeviceEventCallback) EXPECT() *DeviceEventCallback_Expecter { + return &DeviceEventCallback_Expecter{mock: &_m.Mock} +} + +// Execute provides a mock function with given fields: ski, device, event +func (_m *DeviceEventCallback) Execute(ski string, device api.DeviceRemoteInterface, event cemdapi.EventType) { + _m.Called(ski, device, event) +} + +// DeviceEventCallback_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' +type DeviceEventCallback_Execute_Call struct { + *mock.Call +} + +// Execute is a helper method to define mock.On call +// - ski string +// - device api.DeviceRemoteInterface +// - event cemdapi.EventType +func (_e *DeviceEventCallback_Expecter) Execute(ski interface{}, device interface{}, event interface{}) *DeviceEventCallback_Execute_Call { + return &DeviceEventCallback_Execute_Call{Call: _e.mock.On("Execute", ski, device, event)} +} + +func (_c *DeviceEventCallback_Execute_Call) Run(run func(ski string, device api.DeviceRemoteInterface, event cemdapi.EventType)) *DeviceEventCallback_Execute_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(api.DeviceRemoteInterface), args[2].(cemdapi.EventType)) + }) + return _c +} + +func (_c *DeviceEventCallback_Execute_Call) Return() *DeviceEventCallback_Execute_Call { + _c.Call.Return() + return _c +} + +func (_c *DeviceEventCallback_Execute_Call) RunAndReturn(run func(string, api.DeviceRemoteInterface, cemdapi.EventType)) *DeviceEventCallback_Execute_Call { + _c.Call.Return(run) + return _c +} + +// NewDeviceEventCallback creates a new instance of DeviceEventCallback. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewDeviceEventCallback(t interface { + mock.TestingT + Cleanup(func()) +}) *DeviceEventCallback { + mock := &DeviceEventCallback{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/EntityEventCallback.go b/mocks/EntityEventCallback.go new file mode 100644 index 0000000..530b6ab --- /dev/null +++ b/mocks/EntityEventCallback.go @@ -0,0 +1,73 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + cemdapi "github.com/enbility/cemd/api" + api "github.com/enbility/spine-go/api" + + mock "github.com/stretchr/testify/mock" +) + +// EntityEventCallback is an autogenerated mock type for the EntityEventCallback type +type EntityEventCallback struct { + mock.Mock +} + +type EntityEventCallback_Expecter struct { + mock *mock.Mock +} + +func (_m *EntityEventCallback) EXPECT() *EntityEventCallback_Expecter { + return &EntityEventCallback_Expecter{mock: &_m.Mock} +} + +// Execute provides a mock function with given fields: ski, device, entity, event +func (_m *EntityEventCallback) Execute(ski string, device api.DeviceRemoteInterface, entity api.EntityRemoteInterface, event cemdapi.EventType) { + _m.Called(ski, device, entity, event) +} + +// EntityEventCallback_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' +type EntityEventCallback_Execute_Call struct { + *mock.Call +} + +// Execute is a helper method to define mock.On call +// - ski string +// - device api.DeviceRemoteInterface +// - entity api.EntityRemoteInterface +// - event cemdapi.EventType +func (_e *EntityEventCallback_Expecter) Execute(ski interface{}, device interface{}, entity interface{}, event interface{}) *EntityEventCallback_Execute_Call { + return &EntityEventCallback_Execute_Call{Call: _e.mock.On("Execute", ski, device, entity, event)} +} + +func (_c *EntityEventCallback_Execute_Call) Run(run func(ski string, device api.DeviceRemoteInterface, entity api.EntityRemoteInterface, event cemdapi.EventType)) *EntityEventCallback_Execute_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(api.DeviceRemoteInterface), args[2].(api.EntityRemoteInterface), args[3].(cemdapi.EventType)) + }) + return _c +} + +func (_c *EntityEventCallback_Execute_Call) Return() *EntityEventCallback_Execute_Call { + _c.Call.Return() + return _c +} + +func (_c *EntityEventCallback_Execute_Call) RunAndReturn(run func(string, api.DeviceRemoteInterface, api.EntityRemoteInterface, cemdapi.EventType)) *EntityEventCallback_Execute_Call { + _c.Call.Return(run) + return _c +} + +// NewEntityEventCallback creates a new instance of EntityEventCallback. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewEntityEventCallback(t interface { + mock.TestingT + Cleanup(func()) +}) *EntityEventCallback { + mock := &EntityEventCallback{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/UCCEVCInterface.go b/mocks/UCCEVCInterface.go new file mode 100644 index 0000000..99d022f --- /dev/null +++ b/mocks/UCCEVCInterface.go @@ -0,0 +1,706 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + cemdapi "github.com/enbility/cemd/api" + api "github.com/enbility/spine-go/api" + + mock "github.com/stretchr/testify/mock" + + model "github.com/enbility/spine-go/model" +) + +// UCCEVCInterface is an autogenerated mock type for the UCCEVCInterface type +type UCCEVCInterface struct { + mock.Mock +} + +type UCCEVCInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *UCCEVCInterface) EXPECT() *UCCEVCInterface_Expecter { + return &UCCEVCInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *UCCEVCInterface) AddFeatures() { + _m.Called() +} + +// UCCEVCInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type UCCEVCInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *UCCEVCInterface_Expecter) AddFeatures() *UCCEVCInterface_AddFeatures_Call { + return &UCCEVCInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *UCCEVCInterface_AddFeatures_Call) Run(run func()) *UCCEVCInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCCEVCInterface_AddFeatures_Call) Return() *UCCEVCInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *UCCEVCInterface_AddFeatures_Call) RunAndReturn(run func()) *UCCEVCInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *UCCEVCInterface) AddUseCase() { + _m.Called() +} + +// UCCEVCInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type UCCEVCInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *UCCEVCInterface_Expecter) AddUseCase() *UCCEVCInterface_AddUseCase_Call { + return &UCCEVCInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *UCCEVCInterface_AddUseCase_Call) Run(run func()) *UCCEVCInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCCEVCInterface_AddUseCase_Call) Return() *UCCEVCInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *UCCEVCInterface_AddUseCase_Call) RunAndReturn(run func()) *UCCEVCInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// ChargePlan provides a mock function with given fields: entity +func (_m *UCCEVCInterface) ChargePlan(entity api.EntityRemoteInterface) (cemdapi.ChargePlan, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for ChargePlan") + } + + var r0 cemdapi.ChargePlan + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (cemdapi.ChargePlan, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) cemdapi.ChargePlan); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(cemdapi.ChargePlan) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCCEVCInterface_ChargePlan_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChargePlan' +type UCCEVCInterface_ChargePlan_Call struct { + *mock.Call +} + +// ChargePlan is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCCEVCInterface_Expecter) ChargePlan(entity interface{}) *UCCEVCInterface_ChargePlan_Call { + return &UCCEVCInterface_ChargePlan_Call{Call: _e.mock.On("ChargePlan", entity)} +} + +func (_c *UCCEVCInterface_ChargePlan_Call) Run(run func(entity api.EntityRemoteInterface)) *UCCEVCInterface_ChargePlan_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCCEVCInterface_ChargePlan_Call) Return(_a0 cemdapi.ChargePlan, _a1 error) *UCCEVCInterface_ChargePlan_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCCEVCInterface_ChargePlan_Call) RunAndReturn(run func(api.EntityRemoteInterface) (cemdapi.ChargePlan, error)) *UCCEVCInterface_ChargePlan_Call { + _c.Call.Return(run) + return _c +} + +// ChargePlanConstraints provides a mock function with given fields: entity +func (_m *UCCEVCInterface) ChargePlanConstraints(entity api.EntityRemoteInterface) ([]cemdapi.DurationSlotValue, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for ChargePlanConstraints") + } + + var r0 []cemdapi.DurationSlotValue + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) ([]cemdapi.DurationSlotValue, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) []cemdapi.DurationSlotValue); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]cemdapi.DurationSlotValue) + } + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCCEVCInterface_ChargePlanConstraints_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChargePlanConstraints' +type UCCEVCInterface_ChargePlanConstraints_Call struct { + *mock.Call +} + +// ChargePlanConstraints is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCCEVCInterface_Expecter) ChargePlanConstraints(entity interface{}) *UCCEVCInterface_ChargePlanConstraints_Call { + return &UCCEVCInterface_ChargePlanConstraints_Call{Call: _e.mock.On("ChargePlanConstraints", entity)} +} + +func (_c *UCCEVCInterface_ChargePlanConstraints_Call) Run(run func(entity api.EntityRemoteInterface)) *UCCEVCInterface_ChargePlanConstraints_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCCEVCInterface_ChargePlanConstraints_Call) Return(_a0 []cemdapi.DurationSlotValue, _a1 error) *UCCEVCInterface_ChargePlanConstraints_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCCEVCInterface_ChargePlanConstraints_Call) RunAndReturn(run func(api.EntityRemoteInterface) ([]cemdapi.DurationSlotValue, error)) *UCCEVCInterface_ChargePlanConstraints_Call { + _c.Call.Return(run) + return _c +} + +// ChargeStrategy provides a mock function with given fields: remoteEntity +func (_m *UCCEVCInterface) ChargeStrategy(remoteEntity api.EntityRemoteInterface) cemdapi.EVChargeStrategyType { + ret := _m.Called(remoteEntity) + + if len(ret) == 0 { + panic("no return value specified for ChargeStrategy") + } + + var r0 cemdapi.EVChargeStrategyType + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) cemdapi.EVChargeStrategyType); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(cemdapi.EVChargeStrategyType) + } + + return r0 +} + +// UCCEVCInterface_ChargeStrategy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChargeStrategy' +type UCCEVCInterface_ChargeStrategy_Call struct { + *mock.Call +} + +// ChargeStrategy is a helper method to define mock.On call +// - remoteEntity api.EntityRemoteInterface +func (_e *UCCEVCInterface_Expecter) ChargeStrategy(remoteEntity interface{}) *UCCEVCInterface_ChargeStrategy_Call { + return &UCCEVCInterface_ChargeStrategy_Call{Call: _e.mock.On("ChargeStrategy", remoteEntity)} +} + +func (_c *UCCEVCInterface_ChargeStrategy_Call) Run(run func(remoteEntity api.EntityRemoteInterface)) *UCCEVCInterface_ChargeStrategy_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCCEVCInterface_ChargeStrategy_Call) Return(_a0 cemdapi.EVChargeStrategyType) *UCCEVCInterface_ChargeStrategy_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UCCEVCInterface_ChargeStrategy_Call) RunAndReturn(run func(api.EntityRemoteInterface) cemdapi.EVChargeStrategyType) *UCCEVCInterface_ChargeStrategy_Call { + _c.Call.Return(run) + return _c +} + +// EnergyDemand provides a mock function with given fields: remoteEntity +func (_m *UCCEVCInterface) EnergyDemand(remoteEntity api.EntityRemoteInterface) (cemdapi.Demand, error) { + ret := _m.Called(remoteEntity) + + if len(ret) == 0 { + panic("no return value specified for EnergyDemand") + } + + var r0 cemdapi.Demand + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (cemdapi.Demand, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) cemdapi.Demand); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(cemdapi.Demand) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCCEVCInterface_EnergyDemand_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnergyDemand' +type UCCEVCInterface_EnergyDemand_Call struct { + *mock.Call +} + +// EnergyDemand is a helper method to define mock.On call +// - remoteEntity api.EntityRemoteInterface +func (_e *UCCEVCInterface_Expecter) EnergyDemand(remoteEntity interface{}) *UCCEVCInterface_EnergyDemand_Call { + return &UCCEVCInterface_EnergyDemand_Call{Call: _e.mock.On("EnergyDemand", remoteEntity)} +} + +func (_c *UCCEVCInterface_EnergyDemand_Call) Run(run func(remoteEntity api.EntityRemoteInterface)) *UCCEVCInterface_EnergyDemand_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCCEVCInterface_EnergyDemand_Call) Return(_a0 cemdapi.Demand, _a1 error) *UCCEVCInterface_EnergyDemand_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCCEVCInterface_EnergyDemand_Call) RunAndReturn(run func(api.EntityRemoteInterface) (cemdapi.Demand, error)) *UCCEVCInterface_EnergyDemand_Call { + _c.Call.Return(run) + return _c +} + +// IncentiveConstraints provides a mock function with given fields: entity +func (_m *UCCEVCInterface) IncentiveConstraints(entity api.EntityRemoteInterface) (cemdapi.IncentiveSlotConstraints, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for IncentiveConstraints") + } + + var r0 cemdapi.IncentiveSlotConstraints + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (cemdapi.IncentiveSlotConstraints, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) cemdapi.IncentiveSlotConstraints); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(cemdapi.IncentiveSlotConstraints) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCCEVCInterface_IncentiveConstraints_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IncentiveConstraints' +type UCCEVCInterface_IncentiveConstraints_Call struct { + *mock.Call +} + +// IncentiveConstraints is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCCEVCInterface_Expecter) IncentiveConstraints(entity interface{}) *UCCEVCInterface_IncentiveConstraints_Call { + return &UCCEVCInterface_IncentiveConstraints_Call{Call: _e.mock.On("IncentiveConstraints", entity)} +} + +func (_c *UCCEVCInterface_IncentiveConstraints_Call) Run(run func(entity api.EntityRemoteInterface)) *UCCEVCInterface_IncentiveConstraints_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCCEVCInterface_IncentiveConstraints_Call) Return(_a0 cemdapi.IncentiveSlotConstraints, _a1 error) *UCCEVCInterface_IncentiveConstraints_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCCEVCInterface_IncentiveConstraints_Call) RunAndReturn(run func(api.EntityRemoteInterface) (cemdapi.IncentiveSlotConstraints, error)) *UCCEVCInterface_IncentiveConstraints_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *UCCEVCInterface) IsUseCaseSupported(remoteEntity api.EntityRemoteInterface) (bool, error) { + ret := _m.Called(remoteEntity) + + if len(ret) == 0 { + panic("no return value specified for IsUseCaseSupported") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCCEVCInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type UCCEVCInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity api.EntityRemoteInterface +func (_e *UCCEVCInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *UCCEVCInterface_IsUseCaseSupported_Call { + return &UCCEVCInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *UCCEVCInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity api.EntityRemoteInterface)) *UCCEVCInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCCEVCInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *UCCEVCInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCCEVCInterface_IsUseCaseSupported_Call) RunAndReturn(run func(api.EntityRemoteInterface) (bool, error)) *UCCEVCInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// TimeSlotConstraints provides a mock function with given fields: entity +func (_m *UCCEVCInterface) TimeSlotConstraints(entity api.EntityRemoteInterface) (cemdapi.TimeSlotConstraints, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for TimeSlotConstraints") + } + + var r0 cemdapi.TimeSlotConstraints + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (cemdapi.TimeSlotConstraints, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) cemdapi.TimeSlotConstraints); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(cemdapi.TimeSlotConstraints) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCCEVCInterface_TimeSlotConstraints_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TimeSlotConstraints' +type UCCEVCInterface_TimeSlotConstraints_Call struct { + *mock.Call +} + +// TimeSlotConstraints is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCCEVCInterface_Expecter) TimeSlotConstraints(entity interface{}) *UCCEVCInterface_TimeSlotConstraints_Call { + return &UCCEVCInterface_TimeSlotConstraints_Call{Call: _e.mock.On("TimeSlotConstraints", entity)} +} + +func (_c *UCCEVCInterface_TimeSlotConstraints_Call) Run(run func(entity api.EntityRemoteInterface)) *UCCEVCInterface_TimeSlotConstraints_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCCEVCInterface_TimeSlotConstraints_Call) Return(_a0 cemdapi.TimeSlotConstraints, _a1 error) *UCCEVCInterface_TimeSlotConstraints_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCCEVCInterface_TimeSlotConstraints_Call) RunAndReturn(run func(api.EntityRemoteInterface) (cemdapi.TimeSlotConstraints, error)) *UCCEVCInterface_TimeSlotConstraints_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *UCCEVCInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// UCCEVCInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type UCCEVCInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *UCCEVCInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *UCCEVCInterface_UpdateUseCaseAvailability_Call { + return &UCCEVCInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *UCCEVCInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *UCCEVCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *UCCEVCInterface_UpdateUseCaseAvailability_Call) Return() *UCCEVCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *UCCEVCInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *UCCEVCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// UseCaseName provides a mock function with given fields: +func (_m *UCCEVCInterface) UseCaseName() model.UseCaseNameType { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for UseCaseName") + } + + var r0 model.UseCaseNameType + if rf, ok := ret.Get(0).(func() model.UseCaseNameType); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(model.UseCaseNameType) + } + + return r0 +} + +// UCCEVCInterface_UseCaseName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UseCaseName' +type UCCEVCInterface_UseCaseName_Call struct { + *mock.Call +} + +// UseCaseName is a helper method to define mock.On call +func (_e *UCCEVCInterface_Expecter) UseCaseName() *UCCEVCInterface_UseCaseName_Call { + return &UCCEVCInterface_UseCaseName_Call{Call: _e.mock.On("UseCaseName")} +} + +func (_c *UCCEVCInterface_UseCaseName_Call) Run(run func()) *UCCEVCInterface_UseCaseName_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCCEVCInterface_UseCaseName_Call) Return(_a0 model.UseCaseNameType) *UCCEVCInterface_UseCaseName_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UCCEVCInterface_UseCaseName_Call) RunAndReturn(run func() model.UseCaseNameType) *UCCEVCInterface_UseCaseName_Call { + _c.Call.Return(run) + return _c +} + +// WriteIncentiveTableDescriptions provides a mock function with given fields: entity, data +func (_m *UCCEVCInterface) WriteIncentiveTableDescriptions(entity api.EntityRemoteInterface, data []cemdapi.IncentiveTariffDescription) error { + ret := _m.Called(entity, data) + + if len(ret) == 0 { + panic("no return value specified for WriteIncentiveTableDescriptions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface, []cemdapi.IncentiveTariffDescription) error); ok { + r0 = rf(entity, data) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UCCEVCInterface_WriteIncentiveTableDescriptions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteIncentiveTableDescriptions' +type UCCEVCInterface_WriteIncentiveTableDescriptions_Call struct { + *mock.Call +} + +// WriteIncentiveTableDescriptions is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +// - data []cemdapi.IncentiveTariffDescription +func (_e *UCCEVCInterface_Expecter) WriteIncentiveTableDescriptions(entity interface{}, data interface{}) *UCCEVCInterface_WriteIncentiveTableDescriptions_Call { + return &UCCEVCInterface_WriteIncentiveTableDescriptions_Call{Call: _e.mock.On("WriteIncentiveTableDescriptions", entity, data)} +} + +func (_c *UCCEVCInterface_WriteIncentiveTableDescriptions_Call) Run(run func(entity api.EntityRemoteInterface, data []cemdapi.IncentiveTariffDescription)) *UCCEVCInterface_WriteIncentiveTableDescriptions_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface), args[1].([]cemdapi.IncentiveTariffDescription)) + }) + return _c +} + +func (_c *UCCEVCInterface_WriteIncentiveTableDescriptions_Call) Return(_a0 error) *UCCEVCInterface_WriteIncentiveTableDescriptions_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UCCEVCInterface_WriteIncentiveTableDescriptions_Call) RunAndReturn(run func(api.EntityRemoteInterface, []cemdapi.IncentiveTariffDescription) error) *UCCEVCInterface_WriteIncentiveTableDescriptions_Call { + _c.Call.Return(run) + return _c +} + +// WriteIncentives provides a mock function with given fields: entity, data +func (_m *UCCEVCInterface) WriteIncentives(entity api.EntityRemoteInterface, data []cemdapi.DurationSlotValue) error { + ret := _m.Called(entity, data) + + if len(ret) == 0 { + panic("no return value specified for WriteIncentives") + } + + var r0 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface, []cemdapi.DurationSlotValue) error); ok { + r0 = rf(entity, data) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UCCEVCInterface_WriteIncentives_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteIncentives' +type UCCEVCInterface_WriteIncentives_Call struct { + *mock.Call +} + +// WriteIncentives is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +// - data []cemdapi.DurationSlotValue +func (_e *UCCEVCInterface_Expecter) WriteIncentives(entity interface{}, data interface{}) *UCCEVCInterface_WriteIncentives_Call { + return &UCCEVCInterface_WriteIncentives_Call{Call: _e.mock.On("WriteIncentives", entity, data)} +} + +func (_c *UCCEVCInterface_WriteIncentives_Call) Run(run func(entity api.EntityRemoteInterface, data []cemdapi.DurationSlotValue)) *UCCEVCInterface_WriteIncentives_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface), args[1].([]cemdapi.DurationSlotValue)) + }) + return _c +} + +func (_c *UCCEVCInterface_WriteIncentives_Call) Return(_a0 error) *UCCEVCInterface_WriteIncentives_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UCCEVCInterface_WriteIncentives_Call) RunAndReturn(run func(api.EntityRemoteInterface, []cemdapi.DurationSlotValue) error) *UCCEVCInterface_WriteIncentives_Call { + _c.Call.Return(run) + return _c +} + +// WritePowerLimits provides a mock function with given fields: entity, data +func (_m *UCCEVCInterface) WritePowerLimits(entity api.EntityRemoteInterface, data []cemdapi.DurationSlotValue) error { + ret := _m.Called(entity, data) + + if len(ret) == 0 { + panic("no return value specified for WritePowerLimits") + } + + var r0 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface, []cemdapi.DurationSlotValue) error); ok { + r0 = rf(entity, data) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UCCEVCInterface_WritePowerLimits_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WritePowerLimits' +type UCCEVCInterface_WritePowerLimits_Call struct { + *mock.Call +} + +// WritePowerLimits is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +// - data []cemdapi.DurationSlotValue +func (_e *UCCEVCInterface_Expecter) WritePowerLimits(entity interface{}, data interface{}) *UCCEVCInterface_WritePowerLimits_Call { + return &UCCEVCInterface_WritePowerLimits_Call{Call: _e.mock.On("WritePowerLimits", entity, data)} +} + +func (_c *UCCEVCInterface_WritePowerLimits_Call) Run(run func(entity api.EntityRemoteInterface, data []cemdapi.DurationSlotValue)) *UCCEVCInterface_WritePowerLimits_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface), args[1].([]cemdapi.DurationSlotValue)) + }) + return _c +} + +func (_c *UCCEVCInterface_WritePowerLimits_Call) Return(_a0 error) *UCCEVCInterface_WritePowerLimits_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UCCEVCInterface_WritePowerLimits_Call) RunAndReturn(run func(api.EntityRemoteInterface, []cemdapi.DurationSlotValue) error) *UCCEVCInterface_WritePowerLimits_Call { + _c.Call.Return(run) + return _c +} + +// NewUCCEVCInterface creates a new instance of UCCEVCInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewUCCEVCInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *UCCEVCInterface { + mock := &UCCEVCInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/UCEVCCInterface.go b/mocks/UCEVCCInterface.go new file mode 100644 index 0000000..dff64af --- /dev/null +++ b/mocks/UCEVCCInterface.go @@ -0,0 +1,691 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + cemdapi "github.com/enbility/cemd/api" + api "github.com/enbility/spine-go/api" + + mock "github.com/stretchr/testify/mock" + + model "github.com/enbility/spine-go/model" +) + +// UCEVCCInterface is an autogenerated mock type for the UCEVCCInterface type +type UCEVCCInterface struct { + mock.Mock +} + +type UCEVCCInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *UCEVCCInterface) EXPECT() *UCEVCCInterface_Expecter { + return &UCEVCCInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *UCEVCCInterface) AddFeatures() { + _m.Called() +} + +// UCEVCCInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type UCEVCCInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *UCEVCCInterface_Expecter) AddFeatures() *UCEVCCInterface_AddFeatures_Call { + return &UCEVCCInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *UCEVCCInterface_AddFeatures_Call) Run(run func()) *UCEVCCInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCEVCCInterface_AddFeatures_Call) Return() *UCEVCCInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *UCEVCCInterface_AddFeatures_Call) RunAndReturn(run func()) *UCEVCCInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *UCEVCCInterface) AddUseCase() { + _m.Called() +} + +// UCEVCCInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type UCEVCCInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *UCEVCCInterface_Expecter) AddUseCase() *UCEVCCInterface_AddUseCase_Call { + return &UCEVCCInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *UCEVCCInterface_AddUseCase_Call) Run(run func()) *UCEVCCInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCEVCCInterface_AddUseCase_Call) Return() *UCEVCCInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *UCEVCCInterface_AddUseCase_Call) RunAndReturn(run func()) *UCEVCCInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// AsymmetricChargingSupport provides a mock function with given fields: entity +func (_m *UCEVCCInterface) AsymmetricChargingSupport(entity api.EntityRemoteInterface) (bool, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for AsymmetricChargingSupport") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (bool, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) bool); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCEVCCInterface_AsymmetricChargingSupport_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AsymmetricChargingSupport' +type UCEVCCInterface_AsymmetricChargingSupport_Call struct { + *mock.Call +} + +// AsymmetricChargingSupport is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCEVCCInterface_Expecter) AsymmetricChargingSupport(entity interface{}) *UCEVCCInterface_AsymmetricChargingSupport_Call { + return &UCEVCCInterface_AsymmetricChargingSupport_Call{Call: _e.mock.On("AsymmetricChargingSupport", entity)} +} + +func (_c *UCEVCCInterface_AsymmetricChargingSupport_Call) Run(run func(entity api.EntityRemoteInterface)) *UCEVCCInterface_AsymmetricChargingSupport_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCEVCCInterface_AsymmetricChargingSupport_Call) Return(_a0 bool, _a1 error) *UCEVCCInterface_AsymmetricChargingSupport_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCEVCCInterface_AsymmetricChargingSupport_Call) RunAndReturn(run func(api.EntityRemoteInterface) (bool, error)) *UCEVCCInterface_AsymmetricChargingSupport_Call { + _c.Call.Return(run) + return _c +} + +// ChargeState provides a mock function with given fields: entity +func (_m *UCEVCCInterface) ChargeState(entity api.EntityRemoteInterface) (cemdapi.EVChargeStateType, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for ChargeState") + } + + var r0 cemdapi.EVChargeStateType + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (cemdapi.EVChargeStateType, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) cemdapi.EVChargeStateType); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(cemdapi.EVChargeStateType) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCEVCCInterface_ChargeState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChargeState' +type UCEVCCInterface_ChargeState_Call struct { + *mock.Call +} + +// ChargeState is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCEVCCInterface_Expecter) ChargeState(entity interface{}) *UCEVCCInterface_ChargeState_Call { + return &UCEVCCInterface_ChargeState_Call{Call: _e.mock.On("ChargeState", entity)} +} + +func (_c *UCEVCCInterface_ChargeState_Call) Run(run func(entity api.EntityRemoteInterface)) *UCEVCCInterface_ChargeState_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCEVCCInterface_ChargeState_Call) Return(_a0 cemdapi.EVChargeStateType, _a1 error) *UCEVCCInterface_ChargeState_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCEVCCInterface_ChargeState_Call) RunAndReturn(run func(api.EntityRemoteInterface) (cemdapi.EVChargeStateType, error)) *UCEVCCInterface_ChargeState_Call { + _c.Call.Return(run) + return _c +} + +// ChargingPowerLimits provides a mock function with given fields: entity +func (_m *UCEVCCInterface) ChargingPowerLimits(entity api.EntityRemoteInterface) (float64, float64, float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for ChargingPowerLimits") + } + + var r0 float64 + var r1 float64 + var r2 float64 + var r3 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (float64, float64, float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) float64); ok { + r1 = rf(entity) + } else { + r1 = ret.Get(1).(float64) + } + + if rf, ok := ret.Get(2).(func(api.EntityRemoteInterface) float64); ok { + r2 = rf(entity) + } else { + r2 = ret.Get(2).(float64) + } + + if rf, ok := ret.Get(3).(func(api.EntityRemoteInterface) error); ok { + r3 = rf(entity) + } else { + r3 = ret.Error(3) + } + + return r0, r1, r2, r3 +} + +// UCEVCCInterface_ChargingPowerLimits_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChargingPowerLimits' +type UCEVCCInterface_ChargingPowerLimits_Call struct { + *mock.Call +} + +// ChargingPowerLimits is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCEVCCInterface_Expecter) ChargingPowerLimits(entity interface{}) *UCEVCCInterface_ChargingPowerLimits_Call { + return &UCEVCCInterface_ChargingPowerLimits_Call{Call: _e.mock.On("ChargingPowerLimits", entity)} +} + +func (_c *UCEVCCInterface_ChargingPowerLimits_Call) Run(run func(entity api.EntityRemoteInterface)) *UCEVCCInterface_ChargingPowerLimits_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCEVCCInterface_ChargingPowerLimits_Call) Return(_a0 float64, _a1 float64, _a2 float64, _a3 error) *UCEVCCInterface_ChargingPowerLimits_Call { + _c.Call.Return(_a0, _a1, _a2, _a3) + return _c +} + +func (_c *UCEVCCInterface_ChargingPowerLimits_Call) RunAndReturn(run func(api.EntityRemoteInterface) (float64, float64, float64, error)) *UCEVCCInterface_ChargingPowerLimits_Call { + _c.Call.Return(run) + return _c +} + +// CommunicationStandard provides a mock function with given fields: entity +func (_m *UCEVCCInterface) CommunicationStandard(entity api.EntityRemoteInterface) (model.DeviceConfigurationKeyValueStringType, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for CommunicationStandard") + } + + var r0 model.DeviceConfigurationKeyValueStringType + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (model.DeviceConfigurationKeyValueStringType, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) model.DeviceConfigurationKeyValueStringType); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(model.DeviceConfigurationKeyValueStringType) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCEVCCInterface_CommunicationStandard_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CommunicationStandard' +type UCEVCCInterface_CommunicationStandard_Call struct { + *mock.Call +} + +// CommunicationStandard is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCEVCCInterface_Expecter) CommunicationStandard(entity interface{}) *UCEVCCInterface_CommunicationStandard_Call { + return &UCEVCCInterface_CommunicationStandard_Call{Call: _e.mock.On("CommunicationStandard", entity)} +} + +func (_c *UCEVCCInterface_CommunicationStandard_Call) Run(run func(entity api.EntityRemoteInterface)) *UCEVCCInterface_CommunicationStandard_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCEVCCInterface_CommunicationStandard_Call) Return(_a0 model.DeviceConfigurationKeyValueStringType, _a1 error) *UCEVCCInterface_CommunicationStandard_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCEVCCInterface_CommunicationStandard_Call) RunAndReturn(run func(api.EntityRemoteInterface) (model.DeviceConfigurationKeyValueStringType, error)) *UCEVCCInterface_CommunicationStandard_Call { + _c.Call.Return(run) + return _c +} + +// EVConnected provides a mock function with given fields: entity +func (_m *UCEVCCInterface) EVConnected(entity api.EntityRemoteInterface) bool { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for EVConnected") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) bool); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// UCEVCCInterface_EVConnected_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EVConnected' +type UCEVCCInterface_EVConnected_Call struct { + *mock.Call +} + +// EVConnected is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCEVCCInterface_Expecter) EVConnected(entity interface{}) *UCEVCCInterface_EVConnected_Call { + return &UCEVCCInterface_EVConnected_Call{Call: _e.mock.On("EVConnected", entity)} +} + +func (_c *UCEVCCInterface_EVConnected_Call) Run(run func(entity api.EntityRemoteInterface)) *UCEVCCInterface_EVConnected_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCEVCCInterface_EVConnected_Call) Return(_a0 bool) *UCEVCCInterface_EVConnected_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UCEVCCInterface_EVConnected_Call) RunAndReturn(run func(api.EntityRemoteInterface) bool) *UCEVCCInterface_EVConnected_Call { + _c.Call.Return(run) + return _c +} + +// Identifications provides a mock function with given fields: entity +func (_m *UCEVCCInterface) Identifications(entity api.EntityRemoteInterface) ([]cemdapi.IdentificationItem, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for Identifications") + } + + var r0 []cemdapi.IdentificationItem + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) ([]cemdapi.IdentificationItem, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) []cemdapi.IdentificationItem); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]cemdapi.IdentificationItem) + } + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCEVCCInterface_Identifications_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Identifications' +type UCEVCCInterface_Identifications_Call struct { + *mock.Call +} + +// Identifications is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCEVCCInterface_Expecter) Identifications(entity interface{}) *UCEVCCInterface_Identifications_Call { + return &UCEVCCInterface_Identifications_Call{Call: _e.mock.On("Identifications", entity)} +} + +func (_c *UCEVCCInterface_Identifications_Call) Run(run func(entity api.EntityRemoteInterface)) *UCEVCCInterface_Identifications_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCEVCCInterface_Identifications_Call) Return(_a0 []cemdapi.IdentificationItem, _a1 error) *UCEVCCInterface_Identifications_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCEVCCInterface_Identifications_Call) RunAndReturn(run func(api.EntityRemoteInterface) ([]cemdapi.IdentificationItem, error)) *UCEVCCInterface_Identifications_Call { + _c.Call.Return(run) + return _c +} + +// IsInSleepMode provides a mock function with given fields: entity +func (_m *UCEVCCInterface) IsInSleepMode(entity api.EntityRemoteInterface) (bool, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for IsInSleepMode") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (bool, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) bool); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCEVCCInterface_IsInSleepMode_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsInSleepMode' +type UCEVCCInterface_IsInSleepMode_Call struct { + *mock.Call +} + +// IsInSleepMode is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCEVCCInterface_Expecter) IsInSleepMode(entity interface{}) *UCEVCCInterface_IsInSleepMode_Call { + return &UCEVCCInterface_IsInSleepMode_Call{Call: _e.mock.On("IsInSleepMode", entity)} +} + +func (_c *UCEVCCInterface_IsInSleepMode_Call) Run(run func(entity api.EntityRemoteInterface)) *UCEVCCInterface_IsInSleepMode_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCEVCCInterface_IsInSleepMode_Call) Return(_a0 bool, _a1 error) *UCEVCCInterface_IsInSleepMode_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCEVCCInterface_IsInSleepMode_Call) RunAndReturn(run func(api.EntityRemoteInterface) (bool, error)) *UCEVCCInterface_IsInSleepMode_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *UCEVCCInterface) IsUseCaseSupported(remoteEntity api.EntityRemoteInterface) (bool, error) { + ret := _m.Called(remoteEntity) + + if len(ret) == 0 { + panic("no return value specified for IsUseCaseSupported") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCEVCCInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type UCEVCCInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity api.EntityRemoteInterface +func (_e *UCEVCCInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *UCEVCCInterface_IsUseCaseSupported_Call { + return &UCEVCCInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *UCEVCCInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity api.EntityRemoteInterface)) *UCEVCCInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCEVCCInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *UCEVCCInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCEVCCInterface_IsUseCaseSupported_Call) RunAndReturn(run func(api.EntityRemoteInterface) (bool, error)) *UCEVCCInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// ManufacturerData provides a mock function with given fields: entity +func (_m *UCEVCCInterface) ManufacturerData(entity api.EntityRemoteInterface) (cemdapi.ManufacturerData, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for ManufacturerData") + } + + var r0 cemdapi.ManufacturerData + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (cemdapi.ManufacturerData, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) cemdapi.ManufacturerData); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(cemdapi.ManufacturerData) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCEVCCInterface_ManufacturerData_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ManufacturerData' +type UCEVCCInterface_ManufacturerData_Call struct { + *mock.Call +} + +// ManufacturerData is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCEVCCInterface_Expecter) ManufacturerData(entity interface{}) *UCEVCCInterface_ManufacturerData_Call { + return &UCEVCCInterface_ManufacturerData_Call{Call: _e.mock.On("ManufacturerData", entity)} +} + +func (_c *UCEVCCInterface_ManufacturerData_Call) Run(run func(entity api.EntityRemoteInterface)) *UCEVCCInterface_ManufacturerData_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCEVCCInterface_ManufacturerData_Call) Return(_a0 cemdapi.ManufacturerData, _a1 error) *UCEVCCInterface_ManufacturerData_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCEVCCInterface_ManufacturerData_Call) RunAndReturn(run func(api.EntityRemoteInterface) (cemdapi.ManufacturerData, error)) *UCEVCCInterface_ManufacturerData_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *UCEVCCInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// UCEVCCInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type UCEVCCInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *UCEVCCInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *UCEVCCInterface_UpdateUseCaseAvailability_Call { + return &UCEVCCInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *UCEVCCInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *UCEVCCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *UCEVCCInterface_UpdateUseCaseAvailability_Call) Return() *UCEVCCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *UCEVCCInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *UCEVCCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// UseCaseName provides a mock function with given fields: +func (_m *UCEVCCInterface) UseCaseName() model.UseCaseNameType { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for UseCaseName") + } + + var r0 model.UseCaseNameType + if rf, ok := ret.Get(0).(func() model.UseCaseNameType); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(model.UseCaseNameType) + } + + return r0 +} + +// UCEVCCInterface_UseCaseName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UseCaseName' +type UCEVCCInterface_UseCaseName_Call struct { + *mock.Call +} + +// UseCaseName is a helper method to define mock.On call +func (_e *UCEVCCInterface_Expecter) UseCaseName() *UCEVCCInterface_UseCaseName_Call { + return &UCEVCCInterface_UseCaseName_Call{Call: _e.mock.On("UseCaseName")} +} + +func (_c *UCEVCCInterface_UseCaseName_Call) Run(run func()) *UCEVCCInterface_UseCaseName_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCEVCCInterface_UseCaseName_Call) Return(_a0 model.UseCaseNameType) *UCEVCCInterface_UseCaseName_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UCEVCCInterface_UseCaseName_Call) RunAndReturn(run func() model.UseCaseNameType) *UCEVCCInterface_UseCaseName_Call { + _c.Call.Return(run) + return _c +} + +// NewUCEVCCInterface creates a new instance of UCEVCCInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewUCEVCCInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *UCEVCCInterface { + mock := &UCEVCCInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/UCEVCEMInterface.go b/mocks/UCEVCEMInterface.go new file mode 100644 index 0000000..380180d --- /dev/null +++ b/mocks/UCEVCEMInterface.go @@ -0,0 +1,463 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + api "github.com/enbility/spine-go/api" + mock "github.com/stretchr/testify/mock" + + model "github.com/enbility/spine-go/model" +) + +// UCEVCEMInterface is an autogenerated mock type for the UCEVCEMInterface type +type UCEVCEMInterface struct { + mock.Mock +} + +type UCEVCEMInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *UCEVCEMInterface) EXPECT() *UCEVCEMInterface_Expecter { + return &UCEVCEMInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *UCEVCEMInterface) AddFeatures() { + _m.Called() +} + +// UCEVCEMInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type UCEVCEMInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *UCEVCEMInterface_Expecter) AddFeatures() *UCEVCEMInterface_AddFeatures_Call { + return &UCEVCEMInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *UCEVCEMInterface_AddFeatures_Call) Run(run func()) *UCEVCEMInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCEVCEMInterface_AddFeatures_Call) Return() *UCEVCEMInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *UCEVCEMInterface_AddFeatures_Call) RunAndReturn(run func()) *UCEVCEMInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *UCEVCEMInterface) AddUseCase() { + _m.Called() +} + +// UCEVCEMInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type UCEVCEMInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *UCEVCEMInterface_Expecter) AddUseCase() *UCEVCEMInterface_AddUseCase_Call { + return &UCEVCEMInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *UCEVCEMInterface_AddUseCase_Call) Run(run func()) *UCEVCEMInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCEVCEMInterface_AddUseCase_Call) Return() *UCEVCEMInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *UCEVCEMInterface_AddUseCase_Call) RunAndReturn(run func()) *UCEVCEMInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// CurrentPerPhase provides a mock function with given fields: entity +func (_m *UCEVCEMInterface) CurrentPerPhase(entity api.EntityRemoteInterface) ([]float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for CurrentPerPhase") + } + + var r0 []float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) ([]float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) []float64); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]float64) + } + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCEVCEMInterface_CurrentPerPhase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CurrentPerPhase' +type UCEVCEMInterface_CurrentPerPhase_Call struct { + *mock.Call +} + +// CurrentPerPhase is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCEVCEMInterface_Expecter) CurrentPerPhase(entity interface{}) *UCEVCEMInterface_CurrentPerPhase_Call { + return &UCEVCEMInterface_CurrentPerPhase_Call{Call: _e.mock.On("CurrentPerPhase", entity)} +} + +func (_c *UCEVCEMInterface_CurrentPerPhase_Call) Run(run func(entity api.EntityRemoteInterface)) *UCEVCEMInterface_CurrentPerPhase_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCEVCEMInterface_CurrentPerPhase_Call) Return(_a0 []float64, _a1 error) *UCEVCEMInterface_CurrentPerPhase_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCEVCEMInterface_CurrentPerPhase_Call) RunAndReturn(run func(api.EntityRemoteInterface) ([]float64, error)) *UCEVCEMInterface_CurrentPerPhase_Call { + _c.Call.Return(run) + return _c +} + +// EnergyCharged provides a mock function with given fields: entity +func (_m *UCEVCEMInterface) EnergyCharged(entity api.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for EnergyCharged") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCEVCEMInterface_EnergyCharged_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnergyCharged' +type UCEVCEMInterface_EnergyCharged_Call struct { + *mock.Call +} + +// EnergyCharged is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCEVCEMInterface_Expecter) EnergyCharged(entity interface{}) *UCEVCEMInterface_EnergyCharged_Call { + return &UCEVCEMInterface_EnergyCharged_Call{Call: _e.mock.On("EnergyCharged", entity)} +} + +func (_c *UCEVCEMInterface_EnergyCharged_Call) Run(run func(entity api.EntityRemoteInterface)) *UCEVCEMInterface_EnergyCharged_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCEVCEMInterface_EnergyCharged_Call) Return(_a0 float64, _a1 error) *UCEVCEMInterface_EnergyCharged_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCEVCEMInterface_EnergyCharged_Call) RunAndReturn(run func(api.EntityRemoteInterface) (float64, error)) *UCEVCEMInterface_EnergyCharged_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *UCEVCEMInterface) IsUseCaseSupported(remoteEntity api.EntityRemoteInterface) (bool, error) { + ret := _m.Called(remoteEntity) + + if len(ret) == 0 { + panic("no return value specified for IsUseCaseSupported") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCEVCEMInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type UCEVCEMInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity api.EntityRemoteInterface +func (_e *UCEVCEMInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *UCEVCEMInterface_IsUseCaseSupported_Call { + return &UCEVCEMInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *UCEVCEMInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity api.EntityRemoteInterface)) *UCEVCEMInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCEVCEMInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *UCEVCEMInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCEVCEMInterface_IsUseCaseSupported_Call) RunAndReturn(run func(api.EntityRemoteInterface) (bool, error)) *UCEVCEMInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// PhasesConnected provides a mock function with given fields: entity +func (_m *UCEVCEMInterface) PhasesConnected(entity api.EntityRemoteInterface) (uint, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for PhasesConnected") + } + + var r0 uint + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (uint, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) uint); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(uint) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCEVCEMInterface_PhasesConnected_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PhasesConnected' +type UCEVCEMInterface_PhasesConnected_Call struct { + *mock.Call +} + +// PhasesConnected is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCEVCEMInterface_Expecter) PhasesConnected(entity interface{}) *UCEVCEMInterface_PhasesConnected_Call { + return &UCEVCEMInterface_PhasesConnected_Call{Call: _e.mock.On("PhasesConnected", entity)} +} + +func (_c *UCEVCEMInterface_PhasesConnected_Call) Run(run func(entity api.EntityRemoteInterface)) *UCEVCEMInterface_PhasesConnected_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCEVCEMInterface_PhasesConnected_Call) Return(_a0 uint, _a1 error) *UCEVCEMInterface_PhasesConnected_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCEVCEMInterface_PhasesConnected_Call) RunAndReturn(run func(api.EntityRemoteInterface) (uint, error)) *UCEVCEMInterface_PhasesConnected_Call { + _c.Call.Return(run) + return _c +} + +// PowerPerPhase provides a mock function with given fields: entity +func (_m *UCEVCEMInterface) PowerPerPhase(entity api.EntityRemoteInterface) ([]float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for PowerPerPhase") + } + + var r0 []float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) ([]float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) []float64); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]float64) + } + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCEVCEMInterface_PowerPerPhase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PowerPerPhase' +type UCEVCEMInterface_PowerPerPhase_Call struct { + *mock.Call +} + +// PowerPerPhase is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCEVCEMInterface_Expecter) PowerPerPhase(entity interface{}) *UCEVCEMInterface_PowerPerPhase_Call { + return &UCEVCEMInterface_PowerPerPhase_Call{Call: _e.mock.On("PowerPerPhase", entity)} +} + +func (_c *UCEVCEMInterface_PowerPerPhase_Call) Run(run func(entity api.EntityRemoteInterface)) *UCEVCEMInterface_PowerPerPhase_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCEVCEMInterface_PowerPerPhase_Call) Return(_a0 []float64, _a1 error) *UCEVCEMInterface_PowerPerPhase_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCEVCEMInterface_PowerPerPhase_Call) RunAndReturn(run func(api.EntityRemoteInterface) ([]float64, error)) *UCEVCEMInterface_PowerPerPhase_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *UCEVCEMInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// UCEVCEMInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type UCEVCEMInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *UCEVCEMInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *UCEVCEMInterface_UpdateUseCaseAvailability_Call { + return &UCEVCEMInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *UCEVCEMInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *UCEVCEMInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *UCEVCEMInterface_UpdateUseCaseAvailability_Call) Return() *UCEVCEMInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *UCEVCEMInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *UCEVCEMInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// UseCaseName provides a mock function with given fields: +func (_m *UCEVCEMInterface) UseCaseName() model.UseCaseNameType { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for UseCaseName") + } + + var r0 model.UseCaseNameType + if rf, ok := ret.Get(0).(func() model.UseCaseNameType); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(model.UseCaseNameType) + } + + return r0 +} + +// UCEVCEMInterface_UseCaseName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UseCaseName' +type UCEVCEMInterface_UseCaseName_Call struct { + *mock.Call +} + +// UseCaseName is a helper method to define mock.On call +func (_e *UCEVCEMInterface_Expecter) UseCaseName() *UCEVCEMInterface_UseCaseName_Call { + return &UCEVCEMInterface_UseCaseName_Call{Call: _e.mock.On("UseCaseName")} +} + +func (_c *UCEVCEMInterface_UseCaseName_Call) Run(run func()) *UCEVCEMInterface_UseCaseName_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCEVCEMInterface_UseCaseName_Call) Return(_a0 model.UseCaseNameType) *UCEVCEMInterface_UseCaseName_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UCEVCEMInterface_UseCaseName_Call) RunAndReturn(run func() model.UseCaseNameType) *UCEVCEMInterface_UseCaseName_Call { + _c.Call.Return(run) + return _c +} + +// NewUCEVCEMInterface creates a new instance of UCEVCEMInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewUCEVCEMInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *UCEVCEMInterface { + mock := &UCEVCEMInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/UCEVSECCInterface.go b/mocks/UCEVSECCInterface.go new file mode 100644 index 0000000..0b91d79 --- /dev/null +++ b/mocks/UCEVSECCInterface.go @@ -0,0 +1,356 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + cemdapi "github.com/enbility/cemd/api" + api "github.com/enbility/spine-go/api" + + mock "github.com/stretchr/testify/mock" + + model "github.com/enbility/spine-go/model" +) + +// UCEVSECCInterface is an autogenerated mock type for the UCEVSECCInterface type +type UCEVSECCInterface struct { + mock.Mock +} + +type UCEVSECCInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *UCEVSECCInterface) EXPECT() *UCEVSECCInterface_Expecter { + return &UCEVSECCInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *UCEVSECCInterface) AddFeatures() { + _m.Called() +} + +// UCEVSECCInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type UCEVSECCInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *UCEVSECCInterface_Expecter) AddFeatures() *UCEVSECCInterface_AddFeatures_Call { + return &UCEVSECCInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *UCEVSECCInterface_AddFeatures_Call) Run(run func()) *UCEVSECCInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCEVSECCInterface_AddFeatures_Call) Return() *UCEVSECCInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *UCEVSECCInterface_AddFeatures_Call) RunAndReturn(run func()) *UCEVSECCInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *UCEVSECCInterface) AddUseCase() { + _m.Called() +} + +// UCEVSECCInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type UCEVSECCInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *UCEVSECCInterface_Expecter) AddUseCase() *UCEVSECCInterface_AddUseCase_Call { + return &UCEVSECCInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *UCEVSECCInterface_AddUseCase_Call) Run(run func()) *UCEVSECCInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCEVSECCInterface_AddUseCase_Call) Return() *UCEVSECCInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *UCEVSECCInterface_AddUseCase_Call) RunAndReturn(run func()) *UCEVSECCInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *UCEVSECCInterface) IsUseCaseSupported(remoteEntity api.EntityRemoteInterface) (bool, error) { + ret := _m.Called(remoteEntity) + + if len(ret) == 0 { + panic("no return value specified for IsUseCaseSupported") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCEVSECCInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type UCEVSECCInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity api.EntityRemoteInterface +func (_e *UCEVSECCInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *UCEVSECCInterface_IsUseCaseSupported_Call { + return &UCEVSECCInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *UCEVSECCInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity api.EntityRemoteInterface)) *UCEVSECCInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCEVSECCInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *UCEVSECCInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCEVSECCInterface_IsUseCaseSupported_Call) RunAndReturn(run func(api.EntityRemoteInterface) (bool, error)) *UCEVSECCInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// ManufacturerData provides a mock function with given fields: entity +func (_m *UCEVSECCInterface) ManufacturerData(entity api.EntityRemoteInterface) (cemdapi.ManufacturerData, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for ManufacturerData") + } + + var r0 cemdapi.ManufacturerData + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (cemdapi.ManufacturerData, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) cemdapi.ManufacturerData); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(cemdapi.ManufacturerData) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCEVSECCInterface_ManufacturerData_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ManufacturerData' +type UCEVSECCInterface_ManufacturerData_Call struct { + *mock.Call +} + +// ManufacturerData is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCEVSECCInterface_Expecter) ManufacturerData(entity interface{}) *UCEVSECCInterface_ManufacturerData_Call { + return &UCEVSECCInterface_ManufacturerData_Call{Call: _e.mock.On("ManufacturerData", entity)} +} + +func (_c *UCEVSECCInterface_ManufacturerData_Call) Run(run func(entity api.EntityRemoteInterface)) *UCEVSECCInterface_ManufacturerData_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCEVSECCInterface_ManufacturerData_Call) Return(_a0 cemdapi.ManufacturerData, _a1 error) *UCEVSECCInterface_ManufacturerData_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCEVSECCInterface_ManufacturerData_Call) RunAndReturn(run func(api.EntityRemoteInterface) (cemdapi.ManufacturerData, error)) *UCEVSECCInterface_ManufacturerData_Call { + _c.Call.Return(run) + return _c +} + +// OperatingState provides a mock function with given fields: entity +func (_m *UCEVSECCInterface) OperatingState(entity api.EntityRemoteInterface) (model.DeviceDiagnosisOperatingStateType, string, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for OperatingState") + } + + var r0 model.DeviceDiagnosisOperatingStateType + var r1 string + var r2 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (model.DeviceDiagnosisOperatingStateType, string, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) model.DeviceDiagnosisOperatingStateType); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(model.DeviceDiagnosisOperatingStateType) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) string); ok { + r1 = rf(entity) + } else { + r1 = ret.Get(1).(string) + } + + if rf, ok := ret.Get(2).(func(api.EntityRemoteInterface) error); ok { + r2 = rf(entity) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// UCEVSECCInterface_OperatingState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OperatingState' +type UCEVSECCInterface_OperatingState_Call struct { + *mock.Call +} + +// OperatingState is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCEVSECCInterface_Expecter) OperatingState(entity interface{}) *UCEVSECCInterface_OperatingState_Call { + return &UCEVSECCInterface_OperatingState_Call{Call: _e.mock.On("OperatingState", entity)} +} + +func (_c *UCEVSECCInterface_OperatingState_Call) Run(run func(entity api.EntityRemoteInterface)) *UCEVSECCInterface_OperatingState_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCEVSECCInterface_OperatingState_Call) Return(_a0 model.DeviceDiagnosisOperatingStateType, _a1 string, _a2 error) *UCEVSECCInterface_OperatingState_Call { + _c.Call.Return(_a0, _a1, _a2) + return _c +} + +func (_c *UCEVSECCInterface_OperatingState_Call) RunAndReturn(run func(api.EntityRemoteInterface) (model.DeviceDiagnosisOperatingStateType, string, error)) *UCEVSECCInterface_OperatingState_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *UCEVSECCInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// UCEVSECCInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type UCEVSECCInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *UCEVSECCInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *UCEVSECCInterface_UpdateUseCaseAvailability_Call { + return &UCEVSECCInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *UCEVSECCInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *UCEVSECCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *UCEVSECCInterface_UpdateUseCaseAvailability_Call) Return() *UCEVSECCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *UCEVSECCInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *UCEVSECCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// UseCaseName provides a mock function with given fields: +func (_m *UCEVSECCInterface) UseCaseName() model.UseCaseNameType { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for UseCaseName") + } + + var r0 model.UseCaseNameType + if rf, ok := ret.Get(0).(func() model.UseCaseNameType); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(model.UseCaseNameType) + } + + return r0 +} + +// UCEVSECCInterface_UseCaseName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UseCaseName' +type UCEVSECCInterface_UseCaseName_Call struct { + *mock.Call +} + +// UseCaseName is a helper method to define mock.On call +func (_e *UCEVSECCInterface_Expecter) UseCaseName() *UCEVSECCInterface_UseCaseName_Call { + return &UCEVSECCInterface_UseCaseName_Call{Call: _e.mock.On("UseCaseName")} +} + +func (_c *UCEVSECCInterface_UseCaseName_Call) Run(run func()) *UCEVSECCInterface_UseCaseName_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCEVSECCInterface_UseCaseName_Call) Return(_a0 model.UseCaseNameType) *UCEVSECCInterface_UseCaseName_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UCEVSECCInterface_UseCaseName_Call) RunAndReturn(run func() model.UseCaseNameType) *UCEVSECCInterface_UseCaseName_Call { + _c.Call.Return(run) + return _c +} + +// NewUCEVSECCInterface creates a new instance of UCEVSECCInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewUCEVSECCInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *UCEVSECCInterface { + mock := &UCEVSECCInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/UCEVSOCInterface.go b/mocks/UCEVSOCInterface.go new file mode 100644 index 0000000..fb86676 --- /dev/null +++ b/mocks/UCEVSOCInterface.go @@ -0,0 +1,291 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + api "github.com/enbility/spine-go/api" + mock "github.com/stretchr/testify/mock" + + model "github.com/enbility/spine-go/model" +) + +// UCEVSOCInterface is an autogenerated mock type for the UCEVSOCInterface type +type UCEVSOCInterface struct { + mock.Mock +} + +type UCEVSOCInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *UCEVSOCInterface) EXPECT() *UCEVSOCInterface_Expecter { + return &UCEVSOCInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *UCEVSOCInterface) AddFeatures() { + _m.Called() +} + +// UCEVSOCInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type UCEVSOCInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *UCEVSOCInterface_Expecter) AddFeatures() *UCEVSOCInterface_AddFeatures_Call { + return &UCEVSOCInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *UCEVSOCInterface_AddFeatures_Call) Run(run func()) *UCEVSOCInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCEVSOCInterface_AddFeatures_Call) Return() *UCEVSOCInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *UCEVSOCInterface_AddFeatures_Call) RunAndReturn(run func()) *UCEVSOCInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *UCEVSOCInterface) AddUseCase() { + _m.Called() +} + +// UCEVSOCInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type UCEVSOCInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *UCEVSOCInterface_Expecter) AddUseCase() *UCEVSOCInterface_AddUseCase_Call { + return &UCEVSOCInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *UCEVSOCInterface_AddUseCase_Call) Run(run func()) *UCEVSOCInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCEVSOCInterface_AddUseCase_Call) Return() *UCEVSOCInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *UCEVSOCInterface_AddUseCase_Call) RunAndReturn(run func()) *UCEVSOCInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *UCEVSOCInterface) IsUseCaseSupported(remoteEntity api.EntityRemoteInterface) (bool, error) { + ret := _m.Called(remoteEntity) + + if len(ret) == 0 { + panic("no return value specified for IsUseCaseSupported") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCEVSOCInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type UCEVSOCInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity api.EntityRemoteInterface +func (_e *UCEVSOCInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *UCEVSOCInterface_IsUseCaseSupported_Call { + return &UCEVSOCInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *UCEVSOCInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity api.EntityRemoteInterface)) *UCEVSOCInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCEVSOCInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *UCEVSOCInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCEVSOCInterface_IsUseCaseSupported_Call) RunAndReturn(run func(api.EntityRemoteInterface) (bool, error)) *UCEVSOCInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// StateOfCharge provides a mock function with given fields: entity +func (_m *UCEVSOCInterface) StateOfCharge(entity api.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for StateOfCharge") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCEVSOCInterface_StateOfCharge_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StateOfCharge' +type UCEVSOCInterface_StateOfCharge_Call struct { + *mock.Call +} + +// StateOfCharge is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCEVSOCInterface_Expecter) StateOfCharge(entity interface{}) *UCEVSOCInterface_StateOfCharge_Call { + return &UCEVSOCInterface_StateOfCharge_Call{Call: _e.mock.On("StateOfCharge", entity)} +} + +func (_c *UCEVSOCInterface_StateOfCharge_Call) Run(run func(entity api.EntityRemoteInterface)) *UCEVSOCInterface_StateOfCharge_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCEVSOCInterface_StateOfCharge_Call) Return(_a0 float64, _a1 error) *UCEVSOCInterface_StateOfCharge_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCEVSOCInterface_StateOfCharge_Call) RunAndReturn(run func(api.EntityRemoteInterface) (float64, error)) *UCEVSOCInterface_StateOfCharge_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *UCEVSOCInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// UCEVSOCInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type UCEVSOCInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *UCEVSOCInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *UCEVSOCInterface_UpdateUseCaseAvailability_Call { + return &UCEVSOCInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *UCEVSOCInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *UCEVSOCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *UCEVSOCInterface_UpdateUseCaseAvailability_Call) Return() *UCEVSOCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *UCEVSOCInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *UCEVSOCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// UseCaseName provides a mock function with given fields: +func (_m *UCEVSOCInterface) UseCaseName() model.UseCaseNameType { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for UseCaseName") + } + + var r0 model.UseCaseNameType + if rf, ok := ret.Get(0).(func() model.UseCaseNameType); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(model.UseCaseNameType) + } + + return r0 +} + +// UCEVSOCInterface_UseCaseName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UseCaseName' +type UCEVSOCInterface_UseCaseName_Call struct { + *mock.Call +} + +// UseCaseName is a helper method to define mock.On call +func (_e *UCEVSOCInterface_Expecter) UseCaseName() *UCEVSOCInterface_UseCaseName_Call { + return &UCEVSOCInterface_UseCaseName_Call{Call: _e.mock.On("UseCaseName")} +} + +func (_c *UCEVSOCInterface_UseCaseName_Call) Run(run func()) *UCEVSOCInterface_UseCaseName_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCEVSOCInterface_UseCaseName_Call) Return(_a0 model.UseCaseNameType) *UCEVSOCInterface_UseCaseName_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UCEVSOCInterface_UseCaseName_Call) RunAndReturn(run func() model.UseCaseNameType) *UCEVSOCInterface_UseCaseName_Call { + _c.Call.Return(run) + return _c +} + +// NewUCEVSOCInterface creates a new instance of UCEVSOCInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewUCEVSOCInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *UCEVSOCInterface { + mock := &UCEVSOCInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/UCLPCInterface.go b/mocks/UCLPCInterface.go new file mode 100644 index 0000000..d9ff2e8 --- /dev/null +++ b/mocks/UCLPCInterface.go @@ -0,0 +1,640 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + cemdapi "github.com/enbility/cemd/api" + api "github.com/enbility/spine-go/api" + + mock "github.com/stretchr/testify/mock" + + model "github.com/enbility/spine-go/model" + + time "time" +) + +// UCLPCInterface is an autogenerated mock type for the UCLPCInterface type +type UCLPCInterface struct { + mock.Mock +} + +type UCLPCInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *UCLPCInterface) EXPECT() *UCLPCInterface_Expecter { + return &UCLPCInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *UCLPCInterface) AddFeatures() { + _m.Called() +} + +// UCLPCInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type UCLPCInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *UCLPCInterface_Expecter) AddFeatures() *UCLPCInterface_AddFeatures_Call { + return &UCLPCInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *UCLPCInterface_AddFeatures_Call) Run(run func()) *UCLPCInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPCInterface_AddFeatures_Call) Return() *UCLPCInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *UCLPCInterface_AddFeatures_Call) RunAndReturn(run func()) *UCLPCInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *UCLPCInterface) AddUseCase() { + _m.Called() +} + +// UCLPCInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type UCLPCInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *UCLPCInterface_Expecter) AddUseCase() *UCLPCInterface_AddUseCase_Call { + return &UCLPCInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *UCLPCInterface_AddUseCase_Call) Run(run func()) *UCLPCInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPCInterface_AddUseCase_Call) Return() *UCLPCInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *UCLPCInterface_AddUseCase_Call) RunAndReturn(run func()) *UCLPCInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// ConsumptionLimit provides a mock function with given fields: entity +func (_m *UCLPCInterface) ConsumptionLimit(entity api.EntityRemoteInterface) (cemdapi.LoadLimit, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for ConsumptionLimit") + } + + var r0 cemdapi.LoadLimit + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (cemdapi.LoadLimit, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) cemdapi.LoadLimit); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(cemdapi.LoadLimit) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPCInterface_ConsumptionLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ConsumptionLimit' +type UCLPCInterface_ConsumptionLimit_Call struct { + *mock.Call +} + +// ConsumptionLimit is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCLPCInterface_Expecter) ConsumptionLimit(entity interface{}) *UCLPCInterface_ConsumptionLimit_Call { + return &UCLPCInterface_ConsumptionLimit_Call{Call: _e.mock.On("ConsumptionLimit", entity)} +} + +func (_c *UCLPCInterface_ConsumptionLimit_Call) Run(run func(entity api.EntityRemoteInterface)) *UCLPCInterface_ConsumptionLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCLPCInterface_ConsumptionLimit_Call) Return(limit cemdapi.LoadLimit, resultErr error) *UCLPCInterface_ConsumptionLimit_Call { + _c.Call.Return(limit, resultErr) + return _c +} + +func (_c *UCLPCInterface_ConsumptionLimit_Call) RunAndReturn(run func(api.EntityRemoteInterface) (cemdapi.LoadLimit, error)) *UCLPCInterface_ConsumptionLimit_Call { + _c.Call.Return(run) + return _c +} + +// FailsafeConsumptionActivePowerLimit provides a mock function with given fields: entity +func (_m *UCLPCInterface) FailsafeConsumptionActivePowerLimit(entity api.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for FailsafeConsumptionActivePowerLimit") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPCInterface_FailsafeConsumptionActivePowerLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FailsafeConsumptionActivePowerLimit' +type UCLPCInterface_FailsafeConsumptionActivePowerLimit_Call struct { + *mock.Call +} + +// FailsafeConsumptionActivePowerLimit is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCLPCInterface_Expecter) FailsafeConsumptionActivePowerLimit(entity interface{}) *UCLPCInterface_FailsafeConsumptionActivePowerLimit_Call { + return &UCLPCInterface_FailsafeConsumptionActivePowerLimit_Call{Call: _e.mock.On("FailsafeConsumptionActivePowerLimit", entity)} +} + +func (_c *UCLPCInterface_FailsafeConsumptionActivePowerLimit_Call) Run(run func(entity api.EntityRemoteInterface)) *UCLPCInterface_FailsafeConsumptionActivePowerLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCLPCInterface_FailsafeConsumptionActivePowerLimit_Call) Return(_a0 float64, _a1 error) *UCLPCInterface_FailsafeConsumptionActivePowerLimit_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPCInterface_FailsafeConsumptionActivePowerLimit_Call) RunAndReturn(run func(api.EntityRemoteInterface) (float64, error)) *UCLPCInterface_FailsafeConsumptionActivePowerLimit_Call { + _c.Call.Return(run) + return _c +} + +// FailsafeDurationMinimum provides a mock function with given fields: entity +func (_m *UCLPCInterface) FailsafeDurationMinimum(entity api.EntityRemoteInterface) (time.Duration, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for FailsafeDurationMinimum") + } + + var r0 time.Duration + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (time.Duration, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) time.Duration); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(time.Duration) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPCInterface_FailsafeDurationMinimum_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FailsafeDurationMinimum' +type UCLPCInterface_FailsafeDurationMinimum_Call struct { + *mock.Call +} + +// FailsafeDurationMinimum is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCLPCInterface_Expecter) FailsafeDurationMinimum(entity interface{}) *UCLPCInterface_FailsafeDurationMinimum_Call { + return &UCLPCInterface_FailsafeDurationMinimum_Call{Call: _e.mock.On("FailsafeDurationMinimum", entity)} +} + +func (_c *UCLPCInterface_FailsafeDurationMinimum_Call) Run(run func(entity api.EntityRemoteInterface)) *UCLPCInterface_FailsafeDurationMinimum_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCLPCInterface_FailsafeDurationMinimum_Call) Return(_a0 time.Duration, _a1 error) *UCLPCInterface_FailsafeDurationMinimum_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPCInterface_FailsafeDurationMinimum_Call) RunAndReturn(run func(api.EntityRemoteInterface) (time.Duration, error)) *UCLPCInterface_FailsafeDurationMinimum_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *UCLPCInterface) IsUseCaseSupported(remoteEntity api.EntityRemoteInterface) (bool, error) { + ret := _m.Called(remoteEntity) + + if len(ret) == 0 { + panic("no return value specified for IsUseCaseSupported") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPCInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type UCLPCInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity api.EntityRemoteInterface +func (_e *UCLPCInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *UCLPCInterface_IsUseCaseSupported_Call { + return &UCLPCInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *UCLPCInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity api.EntityRemoteInterface)) *UCLPCInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCLPCInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *UCLPCInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPCInterface_IsUseCaseSupported_Call) RunAndReturn(run func(api.EntityRemoteInterface) (bool, error)) *UCLPCInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// PowerConsumptionNominalMax provides a mock function with given fields: entity +func (_m *UCLPCInterface) PowerConsumptionNominalMax(entity api.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for PowerConsumptionNominalMax") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPCInterface_PowerConsumptionNominalMax_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PowerConsumptionNominalMax' +type UCLPCInterface_PowerConsumptionNominalMax_Call struct { + *mock.Call +} + +// PowerConsumptionNominalMax is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCLPCInterface_Expecter) PowerConsumptionNominalMax(entity interface{}) *UCLPCInterface_PowerConsumptionNominalMax_Call { + return &UCLPCInterface_PowerConsumptionNominalMax_Call{Call: _e.mock.On("PowerConsumptionNominalMax", entity)} +} + +func (_c *UCLPCInterface_PowerConsumptionNominalMax_Call) Run(run func(entity api.EntityRemoteInterface)) *UCLPCInterface_PowerConsumptionNominalMax_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCLPCInterface_PowerConsumptionNominalMax_Call) Return(_a0 float64, _a1 error) *UCLPCInterface_PowerConsumptionNominalMax_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPCInterface_PowerConsumptionNominalMax_Call) RunAndReturn(run func(api.EntityRemoteInterface) (float64, error)) *UCLPCInterface_PowerConsumptionNominalMax_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *UCLPCInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// UCLPCInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type UCLPCInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *UCLPCInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *UCLPCInterface_UpdateUseCaseAvailability_Call { + return &UCLPCInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *UCLPCInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *UCLPCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *UCLPCInterface_UpdateUseCaseAvailability_Call) Return() *UCLPCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *UCLPCInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *UCLPCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// UseCaseName provides a mock function with given fields: +func (_m *UCLPCInterface) UseCaseName() model.UseCaseNameType { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for UseCaseName") + } + + var r0 model.UseCaseNameType + if rf, ok := ret.Get(0).(func() model.UseCaseNameType); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(model.UseCaseNameType) + } + + return r0 +} + +// UCLPCInterface_UseCaseName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UseCaseName' +type UCLPCInterface_UseCaseName_Call struct { + *mock.Call +} + +// UseCaseName is a helper method to define mock.On call +func (_e *UCLPCInterface_Expecter) UseCaseName() *UCLPCInterface_UseCaseName_Call { + return &UCLPCInterface_UseCaseName_Call{Call: _e.mock.On("UseCaseName")} +} + +func (_c *UCLPCInterface_UseCaseName_Call) Run(run func()) *UCLPCInterface_UseCaseName_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPCInterface_UseCaseName_Call) Return(_a0 model.UseCaseNameType) *UCLPCInterface_UseCaseName_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UCLPCInterface_UseCaseName_Call) RunAndReturn(run func() model.UseCaseNameType) *UCLPCInterface_UseCaseName_Call { + _c.Call.Return(run) + return _c +} + +// WriteConsumptionLimit provides a mock function with given fields: entity, limit +func (_m *UCLPCInterface) WriteConsumptionLimit(entity api.EntityRemoteInterface, limit cemdapi.LoadLimit) (*model.MsgCounterType, error) { + ret := _m.Called(entity, limit) + + if len(ret) == 0 { + panic("no return value specified for WriteConsumptionLimit") + } + + var r0 *model.MsgCounterType + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface, cemdapi.LoadLimit) (*model.MsgCounterType, error)); ok { + return rf(entity, limit) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface, cemdapi.LoadLimit) *model.MsgCounterType); ok { + r0 = rf(entity, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.MsgCounterType) + } + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface, cemdapi.LoadLimit) error); ok { + r1 = rf(entity, limit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPCInterface_WriteConsumptionLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteConsumptionLimit' +type UCLPCInterface_WriteConsumptionLimit_Call struct { + *mock.Call +} + +// WriteConsumptionLimit is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +// - limit cemdapi.LoadLimit +func (_e *UCLPCInterface_Expecter) WriteConsumptionLimit(entity interface{}, limit interface{}) *UCLPCInterface_WriteConsumptionLimit_Call { + return &UCLPCInterface_WriteConsumptionLimit_Call{Call: _e.mock.On("WriteConsumptionLimit", entity, limit)} +} + +func (_c *UCLPCInterface_WriteConsumptionLimit_Call) Run(run func(entity api.EntityRemoteInterface, limit cemdapi.LoadLimit)) *UCLPCInterface_WriteConsumptionLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface), args[1].(cemdapi.LoadLimit)) + }) + return _c +} + +func (_c *UCLPCInterface_WriteConsumptionLimit_Call) Return(_a0 *model.MsgCounterType, _a1 error) *UCLPCInterface_WriteConsumptionLimit_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPCInterface_WriteConsumptionLimit_Call) RunAndReturn(run func(api.EntityRemoteInterface, cemdapi.LoadLimit) (*model.MsgCounterType, error)) *UCLPCInterface_WriteConsumptionLimit_Call { + _c.Call.Return(run) + return _c +} + +// WriteFailsafeConsumptionActivePowerLimit provides a mock function with given fields: entity, value +func (_m *UCLPCInterface) WriteFailsafeConsumptionActivePowerLimit(entity api.EntityRemoteInterface, value float64) (*model.MsgCounterType, error) { + ret := _m.Called(entity, value) + + if len(ret) == 0 { + panic("no return value specified for WriteFailsafeConsumptionActivePowerLimit") + } + + var r0 *model.MsgCounterType + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface, float64) (*model.MsgCounterType, error)); ok { + return rf(entity, value) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface, float64) *model.MsgCounterType); ok { + r0 = rf(entity, value) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.MsgCounterType) + } + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface, float64) error); ok { + r1 = rf(entity, value) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPCInterface_WriteFailsafeConsumptionActivePowerLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteFailsafeConsumptionActivePowerLimit' +type UCLPCInterface_WriteFailsafeConsumptionActivePowerLimit_Call struct { + *mock.Call +} + +// WriteFailsafeConsumptionActivePowerLimit is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +// - value float64 +func (_e *UCLPCInterface_Expecter) WriteFailsafeConsumptionActivePowerLimit(entity interface{}, value interface{}) *UCLPCInterface_WriteFailsafeConsumptionActivePowerLimit_Call { + return &UCLPCInterface_WriteFailsafeConsumptionActivePowerLimit_Call{Call: _e.mock.On("WriteFailsafeConsumptionActivePowerLimit", entity, value)} +} + +func (_c *UCLPCInterface_WriteFailsafeConsumptionActivePowerLimit_Call) Run(run func(entity api.EntityRemoteInterface, value float64)) *UCLPCInterface_WriteFailsafeConsumptionActivePowerLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface), args[1].(float64)) + }) + return _c +} + +func (_c *UCLPCInterface_WriteFailsafeConsumptionActivePowerLimit_Call) Return(_a0 *model.MsgCounterType, _a1 error) *UCLPCInterface_WriteFailsafeConsumptionActivePowerLimit_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPCInterface_WriteFailsafeConsumptionActivePowerLimit_Call) RunAndReturn(run func(api.EntityRemoteInterface, float64) (*model.MsgCounterType, error)) *UCLPCInterface_WriteFailsafeConsumptionActivePowerLimit_Call { + _c.Call.Return(run) + return _c +} + +// WriteFailsafeDurationMinimum provides a mock function with given fields: entity, duration +func (_m *UCLPCInterface) WriteFailsafeDurationMinimum(entity api.EntityRemoteInterface, duration time.Duration) (*model.MsgCounterType, error) { + ret := _m.Called(entity, duration) + + if len(ret) == 0 { + panic("no return value specified for WriteFailsafeDurationMinimum") + } + + var r0 *model.MsgCounterType + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface, time.Duration) (*model.MsgCounterType, error)); ok { + return rf(entity, duration) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface, time.Duration) *model.MsgCounterType); ok { + r0 = rf(entity, duration) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.MsgCounterType) + } + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface, time.Duration) error); ok { + r1 = rf(entity, duration) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPCInterface_WriteFailsafeDurationMinimum_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteFailsafeDurationMinimum' +type UCLPCInterface_WriteFailsafeDurationMinimum_Call struct { + *mock.Call +} + +// WriteFailsafeDurationMinimum is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +// - duration time.Duration +func (_e *UCLPCInterface_Expecter) WriteFailsafeDurationMinimum(entity interface{}, duration interface{}) *UCLPCInterface_WriteFailsafeDurationMinimum_Call { + return &UCLPCInterface_WriteFailsafeDurationMinimum_Call{Call: _e.mock.On("WriteFailsafeDurationMinimum", entity, duration)} +} + +func (_c *UCLPCInterface_WriteFailsafeDurationMinimum_Call) Run(run func(entity api.EntityRemoteInterface, duration time.Duration)) *UCLPCInterface_WriteFailsafeDurationMinimum_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface), args[1].(time.Duration)) + }) + return _c +} + +func (_c *UCLPCInterface_WriteFailsafeDurationMinimum_Call) Return(_a0 *model.MsgCounterType, _a1 error) *UCLPCInterface_WriteFailsafeDurationMinimum_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPCInterface_WriteFailsafeDurationMinimum_Call) RunAndReturn(run func(api.EntityRemoteInterface, time.Duration) (*model.MsgCounterType, error)) *UCLPCInterface_WriteFailsafeDurationMinimum_Call { + _c.Call.Return(run) + return _c +} + +// NewUCLPCInterface creates a new instance of UCLPCInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewUCLPCInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *UCLPCInterface { + mock := &UCLPCInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/UCLPCServerInterface.go b/mocks/UCLPCServerInterface.go new file mode 100644 index 0000000..b06e28d --- /dev/null +++ b/mocks/UCLPCServerInterface.go @@ -0,0 +1,659 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + api "github.com/enbility/cemd/api" + mock "github.com/stretchr/testify/mock" + + model "github.com/enbility/spine-go/model" + + spine_goapi "github.com/enbility/spine-go/api" + + time "time" +) + +// UCLPCServerInterface is an autogenerated mock type for the UCLPCServerInterface type +type UCLPCServerInterface struct { + mock.Mock +} + +type UCLPCServerInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *UCLPCServerInterface) EXPECT() *UCLPCServerInterface_Expecter { + return &UCLPCServerInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *UCLPCServerInterface) AddFeatures() { + _m.Called() +} + +// UCLPCServerInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type UCLPCServerInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *UCLPCServerInterface_Expecter) AddFeatures() *UCLPCServerInterface_AddFeatures_Call { + return &UCLPCServerInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *UCLPCServerInterface_AddFeatures_Call) Run(run func()) *UCLPCServerInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPCServerInterface_AddFeatures_Call) Return() *UCLPCServerInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *UCLPCServerInterface_AddFeatures_Call) RunAndReturn(run func()) *UCLPCServerInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *UCLPCServerInterface) AddUseCase() { + _m.Called() +} + +// UCLPCServerInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type UCLPCServerInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *UCLPCServerInterface_Expecter) AddUseCase() *UCLPCServerInterface_AddUseCase_Call { + return &UCLPCServerInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *UCLPCServerInterface_AddUseCase_Call) Run(run func()) *UCLPCServerInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPCServerInterface_AddUseCase_Call) Return() *UCLPCServerInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *UCLPCServerInterface_AddUseCase_Call) RunAndReturn(run func()) *UCLPCServerInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// ConsumptionLimit provides a mock function with given fields: +func (_m *UCLPCServerInterface) ConsumptionLimit() (api.LoadLimit, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ConsumptionLimit") + } + + var r0 api.LoadLimit + var r1 error + if rf, ok := ret.Get(0).(func() (api.LoadLimit, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() api.LoadLimit); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(api.LoadLimit) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPCServerInterface_ConsumptionLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ConsumptionLimit' +type UCLPCServerInterface_ConsumptionLimit_Call struct { + *mock.Call +} + +// ConsumptionLimit is a helper method to define mock.On call +func (_e *UCLPCServerInterface_Expecter) ConsumptionLimit() *UCLPCServerInterface_ConsumptionLimit_Call { + return &UCLPCServerInterface_ConsumptionLimit_Call{Call: _e.mock.On("ConsumptionLimit")} +} + +func (_c *UCLPCServerInterface_ConsumptionLimit_Call) Run(run func()) *UCLPCServerInterface_ConsumptionLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPCServerInterface_ConsumptionLimit_Call) Return(_a0 api.LoadLimit, _a1 error) *UCLPCServerInterface_ConsumptionLimit_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPCServerInterface_ConsumptionLimit_Call) RunAndReturn(run func() (api.LoadLimit, error)) *UCLPCServerInterface_ConsumptionLimit_Call { + _c.Call.Return(run) + return _c +} + +// ContractualConsumptionNominalMax provides a mock function with given fields: +func (_m *UCLPCServerInterface) ContractualConsumptionNominalMax() (float64, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ContractualConsumptionNominalMax") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func() (float64, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() float64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPCServerInterface_ContractualConsumptionNominalMax_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ContractualConsumptionNominalMax' +type UCLPCServerInterface_ContractualConsumptionNominalMax_Call struct { + *mock.Call +} + +// ContractualConsumptionNominalMax is a helper method to define mock.On call +func (_e *UCLPCServerInterface_Expecter) ContractualConsumptionNominalMax() *UCLPCServerInterface_ContractualConsumptionNominalMax_Call { + return &UCLPCServerInterface_ContractualConsumptionNominalMax_Call{Call: _e.mock.On("ContractualConsumptionNominalMax")} +} + +func (_c *UCLPCServerInterface_ContractualConsumptionNominalMax_Call) Run(run func()) *UCLPCServerInterface_ContractualConsumptionNominalMax_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPCServerInterface_ContractualConsumptionNominalMax_Call) Return(_a0 float64, _a1 error) *UCLPCServerInterface_ContractualConsumptionNominalMax_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPCServerInterface_ContractualConsumptionNominalMax_Call) RunAndReturn(run func() (float64, error)) *UCLPCServerInterface_ContractualConsumptionNominalMax_Call { + _c.Call.Return(run) + return _c +} + +// FailsafeConsumptionActivePowerLimit provides a mock function with given fields: +func (_m *UCLPCServerInterface) FailsafeConsumptionActivePowerLimit() (float64, bool, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for FailsafeConsumptionActivePowerLimit") + } + + var r0 float64 + var r1 bool + var r2 error + if rf, ok := ret.Get(0).(func() (float64, bool, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() float64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func() bool); ok { + r1 = rf() + } else { + r1 = ret.Get(1).(bool) + } + + if rf, ok := ret.Get(2).(func() error); ok { + r2 = rf() + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// UCLPCServerInterface_FailsafeConsumptionActivePowerLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FailsafeConsumptionActivePowerLimit' +type UCLPCServerInterface_FailsafeConsumptionActivePowerLimit_Call struct { + *mock.Call +} + +// FailsafeConsumptionActivePowerLimit is a helper method to define mock.On call +func (_e *UCLPCServerInterface_Expecter) FailsafeConsumptionActivePowerLimit() *UCLPCServerInterface_FailsafeConsumptionActivePowerLimit_Call { + return &UCLPCServerInterface_FailsafeConsumptionActivePowerLimit_Call{Call: _e.mock.On("FailsafeConsumptionActivePowerLimit")} +} + +func (_c *UCLPCServerInterface_FailsafeConsumptionActivePowerLimit_Call) Run(run func()) *UCLPCServerInterface_FailsafeConsumptionActivePowerLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPCServerInterface_FailsafeConsumptionActivePowerLimit_Call) Return(value float64, isChangeable bool, resultErr error) *UCLPCServerInterface_FailsafeConsumptionActivePowerLimit_Call { + _c.Call.Return(value, isChangeable, resultErr) + return _c +} + +func (_c *UCLPCServerInterface_FailsafeConsumptionActivePowerLimit_Call) RunAndReturn(run func() (float64, bool, error)) *UCLPCServerInterface_FailsafeConsumptionActivePowerLimit_Call { + _c.Call.Return(run) + return _c +} + +// FailsafeDurationMinimum provides a mock function with given fields: +func (_m *UCLPCServerInterface) FailsafeDurationMinimum() (time.Duration, bool, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for FailsafeDurationMinimum") + } + + var r0 time.Duration + var r1 bool + var r2 error + if rf, ok := ret.Get(0).(func() (time.Duration, bool, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() time.Duration); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(time.Duration) + } + + if rf, ok := ret.Get(1).(func() bool); ok { + r1 = rf() + } else { + r1 = ret.Get(1).(bool) + } + + if rf, ok := ret.Get(2).(func() error); ok { + r2 = rf() + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// UCLPCServerInterface_FailsafeDurationMinimum_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FailsafeDurationMinimum' +type UCLPCServerInterface_FailsafeDurationMinimum_Call struct { + *mock.Call +} + +// FailsafeDurationMinimum is a helper method to define mock.On call +func (_e *UCLPCServerInterface_Expecter) FailsafeDurationMinimum() *UCLPCServerInterface_FailsafeDurationMinimum_Call { + return &UCLPCServerInterface_FailsafeDurationMinimum_Call{Call: _e.mock.On("FailsafeDurationMinimum")} +} + +func (_c *UCLPCServerInterface_FailsafeDurationMinimum_Call) Run(run func()) *UCLPCServerInterface_FailsafeDurationMinimum_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPCServerInterface_FailsafeDurationMinimum_Call) Return(duration time.Duration, isChangeable bool, resultErr error) *UCLPCServerInterface_FailsafeDurationMinimum_Call { + _c.Call.Return(duration, isChangeable, resultErr) + return _c +} + +func (_c *UCLPCServerInterface_FailsafeDurationMinimum_Call) RunAndReturn(run func() (time.Duration, bool, error)) *UCLPCServerInterface_FailsafeDurationMinimum_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *UCLPCServerInterface) IsUseCaseSupported(remoteEntity spine_goapi.EntityRemoteInterface) (bool, error) { + ret := _m.Called(remoteEntity) + + if len(ret) == 0 { + panic("no return value specified for IsUseCaseSupported") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPCServerInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type UCLPCServerInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity spine_goapi.EntityRemoteInterface +func (_e *UCLPCServerInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *UCLPCServerInterface_IsUseCaseSupported_Call { + return &UCLPCServerInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *UCLPCServerInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity spine_goapi.EntityRemoteInterface)) *UCLPCServerInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCLPCServerInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *UCLPCServerInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPCServerInterface_IsUseCaseSupported_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (bool, error)) *UCLPCServerInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// SetConsumptionLimit provides a mock function with given fields: limit +func (_m *UCLPCServerInterface) SetConsumptionLimit(limit api.LoadLimit) error { + ret := _m.Called(limit) + + if len(ret) == 0 { + panic("no return value specified for SetConsumptionLimit") + } + + var r0 error + if rf, ok := ret.Get(0).(func(api.LoadLimit) error); ok { + r0 = rf(limit) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UCLPCServerInterface_SetConsumptionLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetConsumptionLimit' +type UCLPCServerInterface_SetConsumptionLimit_Call struct { + *mock.Call +} + +// SetConsumptionLimit is a helper method to define mock.On call +// - limit api.LoadLimit +func (_e *UCLPCServerInterface_Expecter) SetConsumptionLimit(limit interface{}) *UCLPCServerInterface_SetConsumptionLimit_Call { + return &UCLPCServerInterface_SetConsumptionLimit_Call{Call: _e.mock.On("SetConsumptionLimit", limit)} +} + +func (_c *UCLPCServerInterface_SetConsumptionLimit_Call) Run(run func(limit api.LoadLimit)) *UCLPCServerInterface_SetConsumptionLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.LoadLimit)) + }) + return _c +} + +func (_c *UCLPCServerInterface_SetConsumptionLimit_Call) Return(resultErr error) *UCLPCServerInterface_SetConsumptionLimit_Call { + _c.Call.Return(resultErr) + return _c +} + +func (_c *UCLPCServerInterface_SetConsumptionLimit_Call) RunAndReturn(run func(api.LoadLimit) error) *UCLPCServerInterface_SetConsumptionLimit_Call { + _c.Call.Return(run) + return _c +} + +// SetContractualConsumptionNominalMax provides a mock function with given fields: value +func (_m *UCLPCServerInterface) SetContractualConsumptionNominalMax(value float64) error { + ret := _m.Called(value) + + if len(ret) == 0 { + panic("no return value specified for SetContractualConsumptionNominalMax") + } + + var r0 error + if rf, ok := ret.Get(0).(func(float64) error); ok { + r0 = rf(value) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UCLPCServerInterface_SetContractualConsumptionNominalMax_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetContractualConsumptionNominalMax' +type UCLPCServerInterface_SetContractualConsumptionNominalMax_Call struct { + *mock.Call +} + +// SetContractualConsumptionNominalMax is a helper method to define mock.On call +// - value float64 +func (_e *UCLPCServerInterface_Expecter) SetContractualConsumptionNominalMax(value interface{}) *UCLPCServerInterface_SetContractualConsumptionNominalMax_Call { + return &UCLPCServerInterface_SetContractualConsumptionNominalMax_Call{Call: _e.mock.On("SetContractualConsumptionNominalMax", value)} +} + +func (_c *UCLPCServerInterface_SetContractualConsumptionNominalMax_Call) Run(run func(value float64)) *UCLPCServerInterface_SetContractualConsumptionNominalMax_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(float64)) + }) + return _c +} + +func (_c *UCLPCServerInterface_SetContractualConsumptionNominalMax_Call) Return(resultErr error) *UCLPCServerInterface_SetContractualConsumptionNominalMax_Call { + _c.Call.Return(resultErr) + return _c +} + +func (_c *UCLPCServerInterface_SetContractualConsumptionNominalMax_Call) RunAndReturn(run func(float64) error) *UCLPCServerInterface_SetContractualConsumptionNominalMax_Call { + _c.Call.Return(run) + return _c +} + +// SetFailsafeConsumptionActivePowerLimit provides a mock function with given fields: value, changeable +func (_m *UCLPCServerInterface) SetFailsafeConsumptionActivePowerLimit(value float64, changeable bool) error { + ret := _m.Called(value, changeable) + + if len(ret) == 0 { + panic("no return value specified for SetFailsafeConsumptionActivePowerLimit") + } + + var r0 error + if rf, ok := ret.Get(0).(func(float64, bool) error); ok { + r0 = rf(value, changeable) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UCLPCServerInterface_SetFailsafeConsumptionActivePowerLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFailsafeConsumptionActivePowerLimit' +type UCLPCServerInterface_SetFailsafeConsumptionActivePowerLimit_Call struct { + *mock.Call +} + +// SetFailsafeConsumptionActivePowerLimit is a helper method to define mock.On call +// - value float64 +// - changeable bool +func (_e *UCLPCServerInterface_Expecter) SetFailsafeConsumptionActivePowerLimit(value interface{}, changeable interface{}) *UCLPCServerInterface_SetFailsafeConsumptionActivePowerLimit_Call { + return &UCLPCServerInterface_SetFailsafeConsumptionActivePowerLimit_Call{Call: _e.mock.On("SetFailsafeConsumptionActivePowerLimit", value, changeable)} +} + +func (_c *UCLPCServerInterface_SetFailsafeConsumptionActivePowerLimit_Call) Run(run func(value float64, changeable bool)) *UCLPCServerInterface_SetFailsafeConsumptionActivePowerLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(float64), args[1].(bool)) + }) + return _c +} + +func (_c *UCLPCServerInterface_SetFailsafeConsumptionActivePowerLimit_Call) Return(resultErr error) *UCLPCServerInterface_SetFailsafeConsumptionActivePowerLimit_Call { + _c.Call.Return(resultErr) + return _c +} + +func (_c *UCLPCServerInterface_SetFailsafeConsumptionActivePowerLimit_Call) RunAndReturn(run func(float64, bool) error) *UCLPCServerInterface_SetFailsafeConsumptionActivePowerLimit_Call { + _c.Call.Return(run) + return _c +} + +// SetFailsafeDurationMinimum provides a mock function with given fields: duration, changeable +func (_m *UCLPCServerInterface) SetFailsafeDurationMinimum(duration time.Duration, changeable bool) error { + ret := _m.Called(duration, changeable) + + if len(ret) == 0 { + panic("no return value specified for SetFailsafeDurationMinimum") + } + + var r0 error + if rf, ok := ret.Get(0).(func(time.Duration, bool) error); ok { + r0 = rf(duration, changeable) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UCLPCServerInterface_SetFailsafeDurationMinimum_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFailsafeDurationMinimum' +type UCLPCServerInterface_SetFailsafeDurationMinimum_Call struct { + *mock.Call +} + +// SetFailsafeDurationMinimum is a helper method to define mock.On call +// - duration time.Duration +// - changeable bool +func (_e *UCLPCServerInterface_Expecter) SetFailsafeDurationMinimum(duration interface{}, changeable interface{}) *UCLPCServerInterface_SetFailsafeDurationMinimum_Call { + return &UCLPCServerInterface_SetFailsafeDurationMinimum_Call{Call: _e.mock.On("SetFailsafeDurationMinimum", duration, changeable)} +} + +func (_c *UCLPCServerInterface_SetFailsafeDurationMinimum_Call) Run(run func(duration time.Duration, changeable bool)) *UCLPCServerInterface_SetFailsafeDurationMinimum_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(time.Duration), args[1].(bool)) + }) + return _c +} + +func (_c *UCLPCServerInterface_SetFailsafeDurationMinimum_Call) Return(resultErr error) *UCLPCServerInterface_SetFailsafeDurationMinimum_Call { + _c.Call.Return(resultErr) + return _c +} + +func (_c *UCLPCServerInterface_SetFailsafeDurationMinimum_Call) RunAndReturn(run func(time.Duration, bool) error) *UCLPCServerInterface_SetFailsafeDurationMinimum_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *UCLPCServerInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// UCLPCServerInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type UCLPCServerInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *UCLPCServerInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *UCLPCServerInterface_UpdateUseCaseAvailability_Call { + return &UCLPCServerInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *UCLPCServerInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *UCLPCServerInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *UCLPCServerInterface_UpdateUseCaseAvailability_Call) Return() *UCLPCServerInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *UCLPCServerInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *UCLPCServerInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// UseCaseName provides a mock function with given fields: +func (_m *UCLPCServerInterface) UseCaseName() model.UseCaseNameType { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for UseCaseName") + } + + var r0 model.UseCaseNameType + if rf, ok := ret.Get(0).(func() model.UseCaseNameType); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(model.UseCaseNameType) + } + + return r0 +} + +// UCLPCServerInterface_UseCaseName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UseCaseName' +type UCLPCServerInterface_UseCaseName_Call struct { + *mock.Call +} + +// UseCaseName is a helper method to define mock.On call +func (_e *UCLPCServerInterface_Expecter) UseCaseName() *UCLPCServerInterface_UseCaseName_Call { + return &UCLPCServerInterface_UseCaseName_Call{Call: _e.mock.On("UseCaseName")} +} + +func (_c *UCLPCServerInterface_UseCaseName_Call) Run(run func()) *UCLPCServerInterface_UseCaseName_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPCServerInterface_UseCaseName_Call) Return(_a0 model.UseCaseNameType) *UCLPCServerInterface_UseCaseName_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UCLPCServerInterface_UseCaseName_Call) RunAndReturn(run func() model.UseCaseNameType) *UCLPCServerInterface_UseCaseName_Call { + _c.Call.Return(run) + return _c +} + +// NewUCLPCServerInterface creates a new instance of UCLPCServerInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewUCLPCServerInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *UCLPCServerInterface { + mock := &UCLPCServerInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/UCLPPInterface.go b/mocks/UCLPPInterface.go new file mode 100644 index 0000000..fa99ced --- /dev/null +++ b/mocks/UCLPPInterface.go @@ -0,0 +1,640 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + cemdapi "github.com/enbility/cemd/api" + api "github.com/enbility/spine-go/api" + + mock "github.com/stretchr/testify/mock" + + model "github.com/enbility/spine-go/model" + + time "time" +) + +// UCLPPInterface is an autogenerated mock type for the UCLPPInterface type +type UCLPPInterface struct { + mock.Mock +} + +type UCLPPInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *UCLPPInterface) EXPECT() *UCLPPInterface_Expecter { + return &UCLPPInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *UCLPPInterface) AddFeatures() { + _m.Called() +} + +// UCLPPInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type UCLPPInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *UCLPPInterface_Expecter) AddFeatures() *UCLPPInterface_AddFeatures_Call { + return &UCLPPInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *UCLPPInterface_AddFeatures_Call) Run(run func()) *UCLPPInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPPInterface_AddFeatures_Call) Return() *UCLPPInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *UCLPPInterface_AddFeatures_Call) RunAndReturn(run func()) *UCLPPInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *UCLPPInterface) AddUseCase() { + _m.Called() +} + +// UCLPPInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type UCLPPInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *UCLPPInterface_Expecter) AddUseCase() *UCLPPInterface_AddUseCase_Call { + return &UCLPPInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *UCLPPInterface_AddUseCase_Call) Run(run func()) *UCLPPInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPPInterface_AddUseCase_Call) Return() *UCLPPInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *UCLPPInterface_AddUseCase_Call) RunAndReturn(run func()) *UCLPPInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// FailsafeDurationMinimum provides a mock function with given fields: entity +func (_m *UCLPPInterface) FailsafeDurationMinimum(entity api.EntityRemoteInterface) (time.Duration, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for FailsafeDurationMinimum") + } + + var r0 time.Duration + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (time.Duration, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) time.Duration); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(time.Duration) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPPInterface_FailsafeDurationMinimum_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FailsafeDurationMinimum' +type UCLPPInterface_FailsafeDurationMinimum_Call struct { + *mock.Call +} + +// FailsafeDurationMinimum is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCLPPInterface_Expecter) FailsafeDurationMinimum(entity interface{}) *UCLPPInterface_FailsafeDurationMinimum_Call { + return &UCLPPInterface_FailsafeDurationMinimum_Call{Call: _e.mock.On("FailsafeDurationMinimum", entity)} +} + +func (_c *UCLPPInterface_FailsafeDurationMinimum_Call) Run(run func(entity api.EntityRemoteInterface)) *UCLPPInterface_FailsafeDurationMinimum_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCLPPInterface_FailsafeDurationMinimum_Call) Return(_a0 time.Duration, _a1 error) *UCLPPInterface_FailsafeDurationMinimum_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPPInterface_FailsafeDurationMinimum_Call) RunAndReturn(run func(api.EntityRemoteInterface) (time.Duration, error)) *UCLPPInterface_FailsafeDurationMinimum_Call { + _c.Call.Return(run) + return _c +} + +// FailsafeProductionActivePowerLimit provides a mock function with given fields: entity +func (_m *UCLPPInterface) FailsafeProductionActivePowerLimit(entity api.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for FailsafeProductionActivePowerLimit") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPPInterface_FailsafeProductionActivePowerLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FailsafeProductionActivePowerLimit' +type UCLPPInterface_FailsafeProductionActivePowerLimit_Call struct { + *mock.Call +} + +// FailsafeProductionActivePowerLimit is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCLPPInterface_Expecter) FailsafeProductionActivePowerLimit(entity interface{}) *UCLPPInterface_FailsafeProductionActivePowerLimit_Call { + return &UCLPPInterface_FailsafeProductionActivePowerLimit_Call{Call: _e.mock.On("FailsafeProductionActivePowerLimit", entity)} +} + +func (_c *UCLPPInterface_FailsafeProductionActivePowerLimit_Call) Run(run func(entity api.EntityRemoteInterface)) *UCLPPInterface_FailsafeProductionActivePowerLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCLPPInterface_FailsafeProductionActivePowerLimit_Call) Return(_a0 float64, _a1 error) *UCLPPInterface_FailsafeProductionActivePowerLimit_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPPInterface_FailsafeProductionActivePowerLimit_Call) RunAndReturn(run func(api.EntityRemoteInterface) (float64, error)) *UCLPPInterface_FailsafeProductionActivePowerLimit_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *UCLPPInterface) IsUseCaseSupported(remoteEntity api.EntityRemoteInterface) (bool, error) { + ret := _m.Called(remoteEntity) + + if len(ret) == 0 { + panic("no return value specified for IsUseCaseSupported") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPPInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type UCLPPInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity api.EntityRemoteInterface +func (_e *UCLPPInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *UCLPPInterface_IsUseCaseSupported_Call { + return &UCLPPInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *UCLPPInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity api.EntityRemoteInterface)) *UCLPPInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCLPPInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *UCLPPInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPPInterface_IsUseCaseSupported_Call) RunAndReturn(run func(api.EntityRemoteInterface) (bool, error)) *UCLPPInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// PowerProductionNominalMax provides a mock function with given fields: entity +func (_m *UCLPPInterface) PowerProductionNominalMax(entity api.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for PowerProductionNominalMax") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPPInterface_PowerProductionNominalMax_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PowerProductionNominalMax' +type UCLPPInterface_PowerProductionNominalMax_Call struct { + *mock.Call +} + +// PowerProductionNominalMax is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCLPPInterface_Expecter) PowerProductionNominalMax(entity interface{}) *UCLPPInterface_PowerProductionNominalMax_Call { + return &UCLPPInterface_PowerProductionNominalMax_Call{Call: _e.mock.On("PowerProductionNominalMax", entity)} +} + +func (_c *UCLPPInterface_PowerProductionNominalMax_Call) Run(run func(entity api.EntityRemoteInterface)) *UCLPPInterface_PowerProductionNominalMax_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCLPPInterface_PowerProductionNominalMax_Call) Return(_a0 float64, _a1 error) *UCLPPInterface_PowerProductionNominalMax_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPPInterface_PowerProductionNominalMax_Call) RunAndReturn(run func(api.EntityRemoteInterface) (float64, error)) *UCLPPInterface_PowerProductionNominalMax_Call { + _c.Call.Return(run) + return _c +} + +// ProductionLimit provides a mock function with given fields: entity +func (_m *UCLPPInterface) ProductionLimit(entity api.EntityRemoteInterface) (cemdapi.LoadLimit, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for ProductionLimit") + } + + var r0 cemdapi.LoadLimit + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (cemdapi.LoadLimit, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) cemdapi.LoadLimit); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(cemdapi.LoadLimit) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPPInterface_ProductionLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ProductionLimit' +type UCLPPInterface_ProductionLimit_Call struct { + *mock.Call +} + +// ProductionLimit is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCLPPInterface_Expecter) ProductionLimit(entity interface{}) *UCLPPInterface_ProductionLimit_Call { + return &UCLPPInterface_ProductionLimit_Call{Call: _e.mock.On("ProductionLimit", entity)} +} + +func (_c *UCLPPInterface_ProductionLimit_Call) Run(run func(entity api.EntityRemoteInterface)) *UCLPPInterface_ProductionLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCLPPInterface_ProductionLimit_Call) Return(limit cemdapi.LoadLimit, resultErr error) *UCLPPInterface_ProductionLimit_Call { + _c.Call.Return(limit, resultErr) + return _c +} + +func (_c *UCLPPInterface_ProductionLimit_Call) RunAndReturn(run func(api.EntityRemoteInterface) (cemdapi.LoadLimit, error)) *UCLPPInterface_ProductionLimit_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *UCLPPInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// UCLPPInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type UCLPPInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *UCLPPInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *UCLPPInterface_UpdateUseCaseAvailability_Call { + return &UCLPPInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *UCLPPInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *UCLPPInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *UCLPPInterface_UpdateUseCaseAvailability_Call) Return() *UCLPPInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *UCLPPInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *UCLPPInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// UseCaseName provides a mock function with given fields: +func (_m *UCLPPInterface) UseCaseName() model.UseCaseNameType { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for UseCaseName") + } + + var r0 model.UseCaseNameType + if rf, ok := ret.Get(0).(func() model.UseCaseNameType); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(model.UseCaseNameType) + } + + return r0 +} + +// UCLPPInterface_UseCaseName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UseCaseName' +type UCLPPInterface_UseCaseName_Call struct { + *mock.Call +} + +// UseCaseName is a helper method to define mock.On call +func (_e *UCLPPInterface_Expecter) UseCaseName() *UCLPPInterface_UseCaseName_Call { + return &UCLPPInterface_UseCaseName_Call{Call: _e.mock.On("UseCaseName")} +} + +func (_c *UCLPPInterface_UseCaseName_Call) Run(run func()) *UCLPPInterface_UseCaseName_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPPInterface_UseCaseName_Call) Return(_a0 model.UseCaseNameType) *UCLPPInterface_UseCaseName_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UCLPPInterface_UseCaseName_Call) RunAndReturn(run func() model.UseCaseNameType) *UCLPPInterface_UseCaseName_Call { + _c.Call.Return(run) + return _c +} + +// WriteFailsafeDurationMinimum provides a mock function with given fields: entity, duration +func (_m *UCLPPInterface) WriteFailsafeDurationMinimum(entity api.EntityRemoteInterface, duration time.Duration) (*model.MsgCounterType, error) { + ret := _m.Called(entity, duration) + + if len(ret) == 0 { + panic("no return value specified for WriteFailsafeDurationMinimum") + } + + var r0 *model.MsgCounterType + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface, time.Duration) (*model.MsgCounterType, error)); ok { + return rf(entity, duration) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface, time.Duration) *model.MsgCounterType); ok { + r0 = rf(entity, duration) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.MsgCounterType) + } + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface, time.Duration) error); ok { + r1 = rf(entity, duration) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPPInterface_WriteFailsafeDurationMinimum_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteFailsafeDurationMinimum' +type UCLPPInterface_WriteFailsafeDurationMinimum_Call struct { + *mock.Call +} + +// WriteFailsafeDurationMinimum is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +// - duration time.Duration +func (_e *UCLPPInterface_Expecter) WriteFailsafeDurationMinimum(entity interface{}, duration interface{}) *UCLPPInterface_WriteFailsafeDurationMinimum_Call { + return &UCLPPInterface_WriteFailsafeDurationMinimum_Call{Call: _e.mock.On("WriteFailsafeDurationMinimum", entity, duration)} +} + +func (_c *UCLPPInterface_WriteFailsafeDurationMinimum_Call) Run(run func(entity api.EntityRemoteInterface, duration time.Duration)) *UCLPPInterface_WriteFailsafeDurationMinimum_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface), args[1].(time.Duration)) + }) + return _c +} + +func (_c *UCLPPInterface_WriteFailsafeDurationMinimum_Call) Return(_a0 *model.MsgCounterType, _a1 error) *UCLPPInterface_WriteFailsafeDurationMinimum_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPPInterface_WriteFailsafeDurationMinimum_Call) RunAndReturn(run func(api.EntityRemoteInterface, time.Duration) (*model.MsgCounterType, error)) *UCLPPInterface_WriteFailsafeDurationMinimum_Call { + _c.Call.Return(run) + return _c +} + +// WriteFailsafeProductionActivePowerLimit provides a mock function with given fields: entity, value +func (_m *UCLPPInterface) WriteFailsafeProductionActivePowerLimit(entity api.EntityRemoteInterface, value float64) (*model.MsgCounterType, error) { + ret := _m.Called(entity, value) + + if len(ret) == 0 { + panic("no return value specified for WriteFailsafeProductionActivePowerLimit") + } + + var r0 *model.MsgCounterType + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface, float64) (*model.MsgCounterType, error)); ok { + return rf(entity, value) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface, float64) *model.MsgCounterType); ok { + r0 = rf(entity, value) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.MsgCounterType) + } + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface, float64) error); ok { + r1 = rf(entity, value) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPPInterface_WriteFailsafeProductionActivePowerLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteFailsafeProductionActivePowerLimit' +type UCLPPInterface_WriteFailsafeProductionActivePowerLimit_Call struct { + *mock.Call +} + +// WriteFailsafeProductionActivePowerLimit is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +// - value float64 +func (_e *UCLPPInterface_Expecter) WriteFailsafeProductionActivePowerLimit(entity interface{}, value interface{}) *UCLPPInterface_WriteFailsafeProductionActivePowerLimit_Call { + return &UCLPPInterface_WriteFailsafeProductionActivePowerLimit_Call{Call: _e.mock.On("WriteFailsafeProductionActivePowerLimit", entity, value)} +} + +func (_c *UCLPPInterface_WriteFailsafeProductionActivePowerLimit_Call) Run(run func(entity api.EntityRemoteInterface, value float64)) *UCLPPInterface_WriteFailsafeProductionActivePowerLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface), args[1].(float64)) + }) + return _c +} + +func (_c *UCLPPInterface_WriteFailsafeProductionActivePowerLimit_Call) Return(_a0 *model.MsgCounterType, _a1 error) *UCLPPInterface_WriteFailsafeProductionActivePowerLimit_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPPInterface_WriteFailsafeProductionActivePowerLimit_Call) RunAndReturn(run func(api.EntityRemoteInterface, float64) (*model.MsgCounterType, error)) *UCLPPInterface_WriteFailsafeProductionActivePowerLimit_Call { + _c.Call.Return(run) + return _c +} + +// WriteProductionLimit provides a mock function with given fields: entity, limit +func (_m *UCLPPInterface) WriteProductionLimit(entity api.EntityRemoteInterface, limit cemdapi.LoadLimit) (*model.MsgCounterType, error) { + ret := _m.Called(entity, limit) + + if len(ret) == 0 { + panic("no return value specified for WriteProductionLimit") + } + + var r0 *model.MsgCounterType + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface, cemdapi.LoadLimit) (*model.MsgCounterType, error)); ok { + return rf(entity, limit) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface, cemdapi.LoadLimit) *model.MsgCounterType); ok { + r0 = rf(entity, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.MsgCounterType) + } + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface, cemdapi.LoadLimit) error); ok { + r1 = rf(entity, limit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPPInterface_WriteProductionLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteProductionLimit' +type UCLPPInterface_WriteProductionLimit_Call struct { + *mock.Call +} + +// WriteProductionLimit is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +// - limit cemdapi.LoadLimit +func (_e *UCLPPInterface_Expecter) WriteProductionLimit(entity interface{}, limit interface{}) *UCLPPInterface_WriteProductionLimit_Call { + return &UCLPPInterface_WriteProductionLimit_Call{Call: _e.mock.On("WriteProductionLimit", entity, limit)} +} + +func (_c *UCLPPInterface_WriteProductionLimit_Call) Run(run func(entity api.EntityRemoteInterface, limit cemdapi.LoadLimit)) *UCLPPInterface_WriteProductionLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface), args[1].(cemdapi.LoadLimit)) + }) + return _c +} + +func (_c *UCLPPInterface_WriteProductionLimit_Call) Return(_a0 *model.MsgCounterType, _a1 error) *UCLPPInterface_WriteProductionLimit_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPPInterface_WriteProductionLimit_Call) RunAndReturn(run func(api.EntityRemoteInterface, cemdapi.LoadLimit) (*model.MsgCounterType, error)) *UCLPPInterface_WriteProductionLimit_Call { + _c.Call.Return(run) + return _c +} + +// NewUCLPPInterface creates a new instance of UCLPPInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewUCLPPInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *UCLPPInterface { + mock := &UCLPPInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/UCLPPServerInterface.go b/mocks/UCLPPServerInterface.go new file mode 100644 index 0000000..1d6273f --- /dev/null +++ b/mocks/UCLPPServerInterface.go @@ -0,0 +1,659 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + cemdapi "github.com/enbility/cemd/api" + api "github.com/enbility/spine-go/api" + + mock "github.com/stretchr/testify/mock" + + model "github.com/enbility/spine-go/model" + + time "time" +) + +// UCLPPServerInterface is an autogenerated mock type for the UCLPPServerInterface type +type UCLPPServerInterface struct { + mock.Mock +} + +type UCLPPServerInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *UCLPPServerInterface) EXPECT() *UCLPPServerInterface_Expecter { + return &UCLPPServerInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *UCLPPServerInterface) AddFeatures() { + _m.Called() +} + +// UCLPPServerInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type UCLPPServerInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *UCLPPServerInterface_Expecter) AddFeatures() *UCLPPServerInterface_AddFeatures_Call { + return &UCLPPServerInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *UCLPPServerInterface_AddFeatures_Call) Run(run func()) *UCLPPServerInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPPServerInterface_AddFeatures_Call) Return() *UCLPPServerInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *UCLPPServerInterface_AddFeatures_Call) RunAndReturn(run func()) *UCLPPServerInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *UCLPPServerInterface) AddUseCase() { + _m.Called() +} + +// UCLPPServerInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type UCLPPServerInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *UCLPPServerInterface_Expecter) AddUseCase() *UCLPPServerInterface_AddUseCase_Call { + return &UCLPPServerInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *UCLPPServerInterface_AddUseCase_Call) Run(run func()) *UCLPPServerInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPPServerInterface_AddUseCase_Call) Return() *UCLPPServerInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *UCLPPServerInterface_AddUseCase_Call) RunAndReturn(run func()) *UCLPPServerInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// ContractualProductionNominalMax provides a mock function with given fields: +func (_m *UCLPPServerInterface) ContractualProductionNominalMax() (float64, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ContractualProductionNominalMax") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func() (float64, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() float64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPPServerInterface_ContractualProductionNominalMax_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ContractualProductionNominalMax' +type UCLPPServerInterface_ContractualProductionNominalMax_Call struct { + *mock.Call +} + +// ContractualProductionNominalMax is a helper method to define mock.On call +func (_e *UCLPPServerInterface_Expecter) ContractualProductionNominalMax() *UCLPPServerInterface_ContractualProductionNominalMax_Call { + return &UCLPPServerInterface_ContractualProductionNominalMax_Call{Call: _e.mock.On("ContractualProductionNominalMax")} +} + +func (_c *UCLPPServerInterface_ContractualProductionNominalMax_Call) Run(run func()) *UCLPPServerInterface_ContractualProductionNominalMax_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPPServerInterface_ContractualProductionNominalMax_Call) Return(_a0 float64, _a1 error) *UCLPPServerInterface_ContractualProductionNominalMax_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPPServerInterface_ContractualProductionNominalMax_Call) RunAndReturn(run func() (float64, error)) *UCLPPServerInterface_ContractualProductionNominalMax_Call { + _c.Call.Return(run) + return _c +} + +// FailsafeDurationMinimum provides a mock function with given fields: +func (_m *UCLPPServerInterface) FailsafeDurationMinimum() (time.Duration, bool, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for FailsafeDurationMinimum") + } + + var r0 time.Duration + var r1 bool + var r2 error + if rf, ok := ret.Get(0).(func() (time.Duration, bool, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() time.Duration); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(time.Duration) + } + + if rf, ok := ret.Get(1).(func() bool); ok { + r1 = rf() + } else { + r1 = ret.Get(1).(bool) + } + + if rf, ok := ret.Get(2).(func() error); ok { + r2 = rf() + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// UCLPPServerInterface_FailsafeDurationMinimum_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FailsafeDurationMinimum' +type UCLPPServerInterface_FailsafeDurationMinimum_Call struct { + *mock.Call +} + +// FailsafeDurationMinimum is a helper method to define mock.On call +func (_e *UCLPPServerInterface_Expecter) FailsafeDurationMinimum() *UCLPPServerInterface_FailsafeDurationMinimum_Call { + return &UCLPPServerInterface_FailsafeDurationMinimum_Call{Call: _e.mock.On("FailsafeDurationMinimum")} +} + +func (_c *UCLPPServerInterface_FailsafeDurationMinimum_Call) Run(run func()) *UCLPPServerInterface_FailsafeDurationMinimum_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPPServerInterface_FailsafeDurationMinimum_Call) Return(duration time.Duration, isChangeable bool, resultErr error) *UCLPPServerInterface_FailsafeDurationMinimum_Call { + _c.Call.Return(duration, isChangeable, resultErr) + return _c +} + +func (_c *UCLPPServerInterface_FailsafeDurationMinimum_Call) RunAndReturn(run func() (time.Duration, bool, error)) *UCLPPServerInterface_FailsafeDurationMinimum_Call { + _c.Call.Return(run) + return _c +} + +// FailsafeProductionActivePowerLimit provides a mock function with given fields: +func (_m *UCLPPServerInterface) FailsafeProductionActivePowerLimit() (float64, bool, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for FailsafeProductionActivePowerLimit") + } + + var r0 float64 + var r1 bool + var r2 error + if rf, ok := ret.Get(0).(func() (float64, bool, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() float64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func() bool); ok { + r1 = rf() + } else { + r1 = ret.Get(1).(bool) + } + + if rf, ok := ret.Get(2).(func() error); ok { + r2 = rf() + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// UCLPPServerInterface_FailsafeProductionActivePowerLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FailsafeProductionActivePowerLimit' +type UCLPPServerInterface_FailsafeProductionActivePowerLimit_Call struct { + *mock.Call +} + +// FailsafeProductionActivePowerLimit is a helper method to define mock.On call +func (_e *UCLPPServerInterface_Expecter) FailsafeProductionActivePowerLimit() *UCLPPServerInterface_FailsafeProductionActivePowerLimit_Call { + return &UCLPPServerInterface_FailsafeProductionActivePowerLimit_Call{Call: _e.mock.On("FailsafeProductionActivePowerLimit")} +} + +func (_c *UCLPPServerInterface_FailsafeProductionActivePowerLimit_Call) Run(run func()) *UCLPPServerInterface_FailsafeProductionActivePowerLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPPServerInterface_FailsafeProductionActivePowerLimit_Call) Return(value float64, isChangeable bool, resultErr error) *UCLPPServerInterface_FailsafeProductionActivePowerLimit_Call { + _c.Call.Return(value, isChangeable, resultErr) + return _c +} + +func (_c *UCLPPServerInterface_FailsafeProductionActivePowerLimit_Call) RunAndReturn(run func() (float64, bool, error)) *UCLPPServerInterface_FailsafeProductionActivePowerLimit_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *UCLPPServerInterface) IsUseCaseSupported(remoteEntity api.EntityRemoteInterface) (bool, error) { + ret := _m.Called(remoteEntity) + + if len(ret) == 0 { + panic("no return value specified for IsUseCaseSupported") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPPServerInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type UCLPPServerInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity api.EntityRemoteInterface +func (_e *UCLPPServerInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *UCLPPServerInterface_IsUseCaseSupported_Call { + return &UCLPPServerInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *UCLPPServerInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity api.EntityRemoteInterface)) *UCLPPServerInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCLPPServerInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *UCLPPServerInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPPServerInterface_IsUseCaseSupported_Call) RunAndReturn(run func(api.EntityRemoteInterface) (bool, error)) *UCLPPServerInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// ProductionLimit provides a mock function with given fields: +func (_m *UCLPPServerInterface) ProductionLimit() (cemdapi.LoadLimit, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ProductionLimit") + } + + var r0 cemdapi.LoadLimit + var r1 error + if rf, ok := ret.Get(0).(func() (cemdapi.LoadLimit, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() cemdapi.LoadLimit); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(cemdapi.LoadLimit) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPPServerInterface_ProductionLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ProductionLimit' +type UCLPPServerInterface_ProductionLimit_Call struct { + *mock.Call +} + +// ProductionLimit is a helper method to define mock.On call +func (_e *UCLPPServerInterface_Expecter) ProductionLimit() *UCLPPServerInterface_ProductionLimit_Call { + return &UCLPPServerInterface_ProductionLimit_Call{Call: _e.mock.On("ProductionLimit")} +} + +func (_c *UCLPPServerInterface_ProductionLimit_Call) Run(run func()) *UCLPPServerInterface_ProductionLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPPServerInterface_ProductionLimit_Call) Return(_a0 cemdapi.LoadLimit, _a1 error) *UCLPPServerInterface_ProductionLimit_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPPServerInterface_ProductionLimit_Call) RunAndReturn(run func() (cemdapi.LoadLimit, error)) *UCLPPServerInterface_ProductionLimit_Call { + _c.Call.Return(run) + return _c +} + +// SetContractualProductionNominalMax provides a mock function with given fields: value +func (_m *UCLPPServerInterface) SetContractualProductionNominalMax(value float64) error { + ret := _m.Called(value) + + if len(ret) == 0 { + panic("no return value specified for SetContractualProductionNominalMax") + } + + var r0 error + if rf, ok := ret.Get(0).(func(float64) error); ok { + r0 = rf(value) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UCLPPServerInterface_SetContractualProductionNominalMax_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetContractualProductionNominalMax' +type UCLPPServerInterface_SetContractualProductionNominalMax_Call struct { + *mock.Call +} + +// SetContractualProductionNominalMax is a helper method to define mock.On call +// - value float64 +func (_e *UCLPPServerInterface_Expecter) SetContractualProductionNominalMax(value interface{}) *UCLPPServerInterface_SetContractualProductionNominalMax_Call { + return &UCLPPServerInterface_SetContractualProductionNominalMax_Call{Call: _e.mock.On("SetContractualProductionNominalMax", value)} +} + +func (_c *UCLPPServerInterface_SetContractualProductionNominalMax_Call) Run(run func(value float64)) *UCLPPServerInterface_SetContractualProductionNominalMax_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(float64)) + }) + return _c +} + +func (_c *UCLPPServerInterface_SetContractualProductionNominalMax_Call) Return(resultErr error) *UCLPPServerInterface_SetContractualProductionNominalMax_Call { + _c.Call.Return(resultErr) + return _c +} + +func (_c *UCLPPServerInterface_SetContractualProductionNominalMax_Call) RunAndReturn(run func(float64) error) *UCLPPServerInterface_SetContractualProductionNominalMax_Call { + _c.Call.Return(run) + return _c +} + +// SetFailsafeDurationMinimum provides a mock function with given fields: duration, changeable +func (_m *UCLPPServerInterface) SetFailsafeDurationMinimum(duration time.Duration, changeable bool) error { + ret := _m.Called(duration, changeable) + + if len(ret) == 0 { + panic("no return value specified for SetFailsafeDurationMinimum") + } + + var r0 error + if rf, ok := ret.Get(0).(func(time.Duration, bool) error); ok { + r0 = rf(duration, changeable) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UCLPPServerInterface_SetFailsafeDurationMinimum_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFailsafeDurationMinimum' +type UCLPPServerInterface_SetFailsafeDurationMinimum_Call struct { + *mock.Call +} + +// SetFailsafeDurationMinimum is a helper method to define mock.On call +// - duration time.Duration +// - changeable bool +func (_e *UCLPPServerInterface_Expecter) SetFailsafeDurationMinimum(duration interface{}, changeable interface{}) *UCLPPServerInterface_SetFailsafeDurationMinimum_Call { + return &UCLPPServerInterface_SetFailsafeDurationMinimum_Call{Call: _e.mock.On("SetFailsafeDurationMinimum", duration, changeable)} +} + +func (_c *UCLPPServerInterface_SetFailsafeDurationMinimum_Call) Run(run func(duration time.Duration, changeable bool)) *UCLPPServerInterface_SetFailsafeDurationMinimum_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(time.Duration), args[1].(bool)) + }) + return _c +} + +func (_c *UCLPPServerInterface_SetFailsafeDurationMinimum_Call) Return(resultErr error) *UCLPPServerInterface_SetFailsafeDurationMinimum_Call { + _c.Call.Return(resultErr) + return _c +} + +func (_c *UCLPPServerInterface_SetFailsafeDurationMinimum_Call) RunAndReturn(run func(time.Duration, bool) error) *UCLPPServerInterface_SetFailsafeDurationMinimum_Call { + _c.Call.Return(run) + return _c +} + +// SetFailsafeProductionActivePowerLimit provides a mock function with given fields: value, changeable +func (_m *UCLPPServerInterface) SetFailsafeProductionActivePowerLimit(value float64, changeable bool) error { + ret := _m.Called(value, changeable) + + if len(ret) == 0 { + panic("no return value specified for SetFailsafeProductionActivePowerLimit") + } + + var r0 error + if rf, ok := ret.Get(0).(func(float64, bool) error); ok { + r0 = rf(value, changeable) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UCLPPServerInterface_SetFailsafeProductionActivePowerLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFailsafeProductionActivePowerLimit' +type UCLPPServerInterface_SetFailsafeProductionActivePowerLimit_Call struct { + *mock.Call +} + +// SetFailsafeProductionActivePowerLimit is a helper method to define mock.On call +// - value float64 +// - changeable bool +func (_e *UCLPPServerInterface_Expecter) SetFailsafeProductionActivePowerLimit(value interface{}, changeable interface{}) *UCLPPServerInterface_SetFailsafeProductionActivePowerLimit_Call { + return &UCLPPServerInterface_SetFailsafeProductionActivePowerLimit_Call{Call: _e.mock.On("SetFailsafeProductionActivePowerLimit", value, changeable)} +} + +func (_c *UCLPPServerInterface_SetFailsafeProductionActivePowerLimit_Call) Run(run func(value float64, changeable bool)) *UCLPPServerInterface_SetFailsafeProductionActivePowerLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(float64), args[1].(bool)) + }) + return _c +} + +func (_c *UCLPPServerInterface_SetFailsafeProductionActivePowerLimit_Call) Return(resultErr error) *UCLPPServerInterface_SetFailsafeProductionActivePowerLimit_Call { + _c.Call.Return(resultErr) + return _c +} + +func (_c *UCLPPServerInterface_SetFailsafeProductionActivePowerLimit_Call) RunAndReturn(run func(float64, bool) error) *UCLPPServerInterface_SetFailsafeProductionActivePowerLimit_Call { + _c.Call.Return(run) + return _c +} + +// SetProductionLimit provides a mock function with given fields: limit +func (_m *UCLPPServerInterface) SetProductionLimit(limit cemdapi.LoadLimit) error { + ret := _m.Called(limit) + + if len(ret) == 0 { + panic("no return value specified for SetProductionLimit") + } + + var r0 error + if rf, ok := ret.Get(0).(func(cemdapi.LoadLimit) error); ok { + r0 = rf(limit) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UCLPPServerInterface_SetProductionLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetProductionLimit' +type UCLPPServerInterface_SetProductionLimit_Call struct { + *mock.Call +} + +// SetProductionLimit is a helper method to define mock.On call +// - limit cemdapi.LoadLimit +func (_e *UCLPPServerInterface_Expecter) SetProductionLimit(limit interface{}) *UCLPPServerInterface_SetProductionLimit_Call { + return &UCLPPServerInterface_SetProductionLimit_Call{Call: _e.mock.On("SetProductionLimit", limit)} +} + +func (_c *UCLPPServerInterface_SetProductionLimit_Call) Run(run func(limit cemdapi.LoadLimit)) *UCLPPServerInterface_SetProductionLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(cemdapi.LoadLimit)) + }) + return _c +} + +func (_c *UCLPPServerInterface_SetProductionLimit_Call) Return(resultErr error) *UCLPPServerInterface_SetProductionLimit_Call { + _c.Call.Return(resultErr) + return _c +} + +func (_c *UCLPPServerInterface_SetProductionLimit_Call) RunAndReturn(run func(cemdapi.LoadLimit) error) *UCLPPServerInterface_SetProductionLimit_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *UCLPPServerInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// UCLPPServerInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type UCLPPServerInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *UCLPPServerInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *UCLPPServerInterface_UpdateUseCaseAvailability_Call { + return &UCLPPServerInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *UCLPPServerInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *UCLPPServerInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *UCLPPServerInterface_UpdateUseCaseAvailability_Call) Return() *UCLPPServerInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *UCLPPServerInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *UCLPPServerInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// UseCaseName provides a mock function with given fields: +func (_m *UCLPPServerInterface) UseCaseName() model.UseCaseNameType { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for UseCaseName") + } + + var r0 model.UseCaseNameType + if rf, ok := ret.Get(0).(func() model.UseCaseNameType); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(model.UseCaseNameType) + } + + return r0 +} + +// UCLPPServerInterface_UseCaseName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UseCaseName' +type UCLPPServerInterface_UseCaseName_Call struct { + *mock.Call +} + +// UseCaseName is a helper method to define mock.On call +func (_e *UCLPPServerInterface_Expecter) UseCaseName() *UCLPPServerInterface_UseCaseName_Call { + return &UCLPPServerInterface_UseCaseName_Call{Call: _e.mock.On("UseCaseName")} +} + +func (_c *UCLPPServerInterface_UseCaseName_Call) Run(run func()) *UCLPPServerInterface_UseCaseName_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPPServerInterface_UseCaseName_Call) Return(_a0 model.UseCaseNameType) *UCLPPServerInterface_UseCaseName_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UCLPPServerInterface_UseCaseName_Call) RunAndReturn(run func() model.UseCaseNameType) *UCLPPServerInterface_UseCaseName_Call { + _c.Call.Return(run) + return _c +} + +// NewUCLPPServerInterface creates a new instance of UCLPPServerInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewUCLPPServerInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *UCLPPServerInterface { + mock := &UCLPPServerInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/UCMCPInterface.go b/mocks/UCMCPInterface.go new file mode 100644 index 0000000..4afdefb --- /dev/null +++ b/mocks/UCMCPInterface.go @@ -0,0 +1,633 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + api "github.com/enbility/spine-go/api" + mock "github.com/stretchr/testify/mock" + + model "github.com/enbility/spine-go/model" +) + +// UCMCPInterface is an autogenerated mock type for the UCMCPInterface type +type UCMCPInterface struct { + mock.Mock +} + +type UCMCPInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *UCMCPInterface) EXPECT() *UCMCPInterface_Expecter { + return &UCMCPInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *UCMCPInterface) AddFeatures() { + _m.Called() +} + +// UCMCPInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type UCMCPInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *UCMCPInterface_Expecter) AddFeatures() *UCMCPInterface_AddFeatures_Call { + return &UCMCPInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *UCMCPInterface_AddFeatures_Call) Run(run func()) *UCMCPInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCMCPInterface_AddFeatures_Call) Return() *UCMCPInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *UCMCPInterface_AddFeatures_Call) RunAndReturn(run func()) *UCMCPInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *UCMCPInterface) AddUseCase() { + _m.Called() +} + +// UCMCPInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type UCMCPInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *UCMCPInterface_Expecter) AddUseCase() *UCMCPInterface_AddUseCase_Call { + return &UCMCPInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *UCMCPInterface_AddUseCase_Call) Run(run func()) *UCMCPInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCMCPInterface_AddUseCase_Call) Return() *UCMCPInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *UCMCPInterface_AddUseCase_Call) RunAndReturn(run func()) *UCMCPInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// CurrentPerPhase provides a mock function with given fields: entity +func (_m *UCMCPInterface) CurrentPerPhase(entity api.EntityRemoteInterface) ([]float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for CurrentPerPhase") + } + + var r0 []float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) ([]float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) []float64); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]float64) + } + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCMCPInterface_CurrentPerPhase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CurrentPerPhase' +type UCMCPInterface_CurrentPerPhase_Call struct { + *mock.Call +} + +// CurrentPerPhase is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCMCPInterface_Expecter) CurrentPerPhase(entity interface{}) *UCMCPInterface_CurrentPerPhase_Call { + return &UCMCPInterface_CurrentPerPhase_Call{Call: _e.mock.On("CurrentPerPhase", entity)} +} + +func (_c *UCMCPInterface_CurrentPerPhase_Call) Run(run func(entity api.EntityRemoteInterface)) *UCMCPInterface_CurrentPerPhase_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCMCPInterface_CurrentPerPhase_Call) Return(_a0 []float64, _a1 error) *UCMCPInterface_CurrentPerPhase_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCMCPInterface_CurrentPerPhase_Call) RunAndReturn(run func(api.EntityRemoteInterface) ([]float64, error)) *UCMCPInterface_CurrentPerPhase_Call { + _c.Call.Return(run) + return _c +} + +// EnergyConsumed provides a mock function with given fields: entity +func (_m *UCMCPInterface) EnergyConsumed(entity api.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for EnergyConsumed") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCMCPInterface_EnergyConsumed_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnergyConsumed' +type UCMCPInterface_EnergyConsumed_Call struct { + *mock.Call +} + +// EnergyConsumed is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCMCPInterface_Expecter) EnergyConsumed(entity interface{}) *UCMCPInterface_EnergyConsumed_Call { + return &UCMCPInterface_EnergyConsumed_Call{Call: _e.mock.On("EnergyConsumed", entity)} +} + +func (_c *UCMCPInterface_EnergyConsumed_Call) Run(run func(entity api.EntityRemoteInterface)) *UCMCPInterface_EnergyConsumed_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCMCPInterface_EnergyConsumed_Call) Return(_a0 float64, _a1 error) *UCMCPInterface_EnergyConsumed_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCMCPInterface_EnergyConsumed_Call) RunAndReturn(run func(api.EntityRemoteInterface) (float64, error)) *UCMCPInterface_EnergyConsumed_Call { + _c.Call.Return(run) + return _c +} + +// EnergyProduced provides a mock function with given fields: entity +func (_m *UCMCPInterface) EnergyProduced(entity api.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for EnergyProduced") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCMCPInterface_EnergyProduced_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnergyProduced' +type UCMCPInterface_EnergyProduced_Call struct { + *mock.Call +} + +// EnergyProduced is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCMCPInterface_Expecter) EnergyProduced(entity interface{}) *UCMCPInterface_EnergyProduced_Call { + return &UCMCPInterface_EnergyProduced_Call{Call: _e.mock.On("EnergyProduced", entity)} +} + +func (_c *UCMCPInterface_EnergyProduced_Call) Run(run func(entity api.EntityRemoteInterface)) *UCMCPInterface_EnergyProduced_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCMCPInterface_EnergyProduced_Call) Return(_a0 float64, _a1 error) *UCMCPInterface_EnergyProduced_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCMCPInterface_EnergyProduced_Call) RunAndReturn(run func(api.EntityRemoteInterface) (float64, error)) *UCMCPInterface_EnergyProduced_Call { + _c.Call.Return(run) + return _c +} + +// Frequency provides a mock function with given fields: entity +func (_m *UCMCPInterface) Frequency(entity api.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for Frequency") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCMCPInterface_Frequency_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Frequency' +type UCMCPInterface_Frequency_Call struct { + *mock.Call +} + +// Frequency is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCMCPInterface_Expecter) Frequency(entity interface{}) *UCMCPInterface_Frequency_Call { + return &UCMCPInterface_Frequency_Call{Call: _e.mock.On("Frequency", entity)} +} + +func (_c *UCMCPInterface_Frequency_Call) Run(run func(entity api.EntityRemoteInterface)) *UCMCPInterface_Frequency_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCMCPInterface_Frequency_Call) Return(_a0 float64, _a1 error) *UCMCPInterface_Frequency_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCMCPInterface_Frequency_Call) RunAndReturn(run func(api.EntityRemoteInterface) (float64, error)) *UCMCPInterface_Frequency_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *UCMCPInterface) IsUseCaseSupported(remoteEntity api.EntityRemoteInterface) (bool, error) { + ret := _m.Called(remoteEntity) + + if len(ret) == 0 { + panic("no return value specified for IsUseCaseSupported") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCMCPInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type UCMCPInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity api.EntityRemoteInterface +func (_e *UCMCPInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *UCMCPInterface_IsUseCaseSupported_Call { + return &UCMCPInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *UCMCPInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity api.EntityRemoteInterface)) *UCMCPInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCMCPInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *UCMCPInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCMCPInterface_IsUseCaseSupported_Call) RunAndReturn(run func(api.EntityRemoteInterface) (bool, error)) *UCMCPInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// Power provides a mock function with given fields: entity +func (_m *UCMCPInterface) Power(entity api.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for Power") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCMCPInterface_Power_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Power' +type UCMCPInterface_Power_Call struct { + *mock.Call +} + +// Power is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCMCPInterface_Expecter) Power(entity interface{}) *UCMCPInterface_Power_Call { + return &UCMCPInterface_Power_Call{Call: _e.mock.On("Power", entity)} +} + +func (_c *UCMCPInterface_Power_Call) Run(run func(entity api.EntityRemoteInterface)) *UCMCPInterface_Power_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCMCPInterface_Power_Call) Return(_a0 float64, _a1 error) *UCMCPInterface_Power_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCMCPInterface_Power_Call) RunAndReturn(run func(api.EntityRemoteInterface) (float64, error)) *UCMCPInterface_Power_Call { + _c.Call.Return(run) + return _c +} + +// PowerPerPhase provides a mock function with given fields: entity +func (_m *UCMCPInterface) PowerPerPhase(entity api.EntityRemoteInterface) ([]float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for PowerPerPhase") + } + + var r0 []float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) ([]float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) []float64); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]float64) + } + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCMCPInterface_PowerPerPhase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PowerPerPhase' +type UCMCPInterface_PowerPerPhase_Call struct { + *mock.Call +} + +// PowerPerPhase is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCMCPInterface_Expecter) PowerPerPhase(entity interface{}) *UCMCPInterface_PowerPerPhase_Call { + return &UCMCPInterface_PowerPerPhase_Call{Call: _e.mock.On("PowerPerPhase", entity)} +} + +func (_c *UCMCPInterface_PowerPerPhase_Call) Run(run func(entity api.EntityRemoteInterface)) *UCMCPInterface_PowerPerPhase_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCMCPInterface_PowerPerPhase_Call) Return(_a0 []float64, _a1 error) *UCMCPInterface_PowerPerPhase_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCMCPInterface_PowerPerPhase_Call) RunAndReturn(run func(api.EntityRemoteInterface) ([]float64, error)) *UCMCPInterface_PowerPerPhase_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *UCMCPInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// UCMCPInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type UCMCPInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *UCMCPInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *UCMCPInterface_UpdateUseCaseAvailability_Call { + return &UCMCPInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *UCMCPInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *UCMCPInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *UCMCPInterface_UpdateUseCaseAvailability_Call) Return() *UCMCPInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *UCMCPInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *UCMCPInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// UseCaseName provides a mock function with given fields: +func (_m *UCMCPInterface) UseCaseName() model.UseCaseNameType { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for UseCaseName") + } + + var r0 model.UseCaseNameType + if rf, ok := ret.Get(0).(func() model.UseCaseNameType); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(model.UseCaseNameType) + } + + return r0 +} + +// UCMCPInterface_UseCaseName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UseCaseName' +type UCMCPInterface_UseCaseName_Call struct { + *mock.Call +} + +// UseCaseName is a helper method to define mock.On call +func (_e *UCMCPInterface_Expecter) UseCaseName() *UCMCPInterface_UseCaseName_Call { + return &UCMCPInterface_UseCaseName_Call{Call: _e.mock.On("UseCaseName")} +} + +func (_c *UCMCPInterface_UseCaseName_Call) Run(run func()) *UCMCPInterface_UseCaseName_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCMCPInterface_UseCaseName_Call) Return(_a0 model.UseCaseNameType) *UCMCPInterface_UseCaseName_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UCMCPInterface_UseCaseName_Call) RunAndReturn(run func() model.UseCaseNameType) *UCMCPInterface_UseCaseName_Call { + _c.Call.Return(run) + return _c +} + +// VoltagePerPhase provides a mock function with given fields: entity +func (_m *UCMCPInterface) VoltagePerPhase(entity api.EntityRemoteInterface) ([]float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for VoltagePerPhase") + } + + var r0 []float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) ([]float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) []float64); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]float64) + } + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCMCPInterface_VoltagePerPhase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VoltagePerPhase' +type UCMCPInterface_VoltagePerPhase_Call struct { + *mock.Call +} + +// VoltagePerPhase is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCMCPInterface_Expecter) VoltagePerPhase(entity interface{}) *UCMCPInterface_VoltagePerPhase_Call { + return &UCMCPInterface_VoltagePerPhase_Call{Call: _e.mock.On("VoltagePerPhase", entity)} +} + +func (_c *UCMCPInterface_VoltagePerPhase_Call) Run(run func(entity api.EntityRemoteInterface)) *UCMCPInterface_VoltagePerPhase_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCMCPInterface_VoltagePerPhase_Call) Return(_a0 []float64, _a1 error) *UCMCPInterface_VoltagePerPhase_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCMCPInterface_VoltagePerPhase_Call) RunAndReturn(run func(api.EntityRemoteInterface) ([]float64, error)) *UCMCPInterface_VoltagePerPhase_Call { + _c.Call.Return(run) + return _c +} + +// NewUCMCPInterface creates a new instance of UCMCPInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewUCMCPInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *UCMCPInterface { + mock := &UCMCPInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/UCMGCPInterface.go b/mocks/UCMGCPInterface.go new file mode 100644 index 0000000..5c5d0bf --- /dev/null +++ b/mocks/UCMGCPInterface.go @@ -0,0 +1,631 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + api "github.com/enbility/spine-go/api" + mock "github.com/stretchr/testify/mock" + + model "github.com/enbility/spine-go/model" +) + +// UCMGCPInterface is an autogenerated mock type for the UCMGCPInterface type +type UCMGCPInterface struct { + mock.Mock +} + +type UCMGCPInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *UCMGCPInterface) EXPECT() *UCMGCPInterface_Expecter { + return &UCMGCPInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *UCMGCPInterface) AddFeatures() { + _m.Called() +} + +// UCMGCPInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type UCMGCPInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *UCMGCPInterface_Expecter) AddFeatures() *UCMGCPInterface_AddFeatures_Call { + return &UCMGCPInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *UCMGCPInterface_AddFeatures_Call) Run(run func()) *UCMGCPInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCMGCPInterface_AddFeatures_Call) Return() *UCMGCPInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *UCMGCPInterface_AddFeatures_Call) RunAndReturn(run func()) *UCMGCPInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *UCMGCPInterface) AddUseCase() { + _m.Called() +} + +// UCMGCPInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type UCMGCPInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *UCMGCPInterface_Expecter) AddUseCase() *UCMGCPInterface_AddUseCase_Call { + return &UCMGCPInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *UCMGCPInterface_AddUseCase_Call) Run(run func()) *UCMGCPInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCMGCPInterface_AddUseCase_Call) Return() *UCMGCPInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *UCMGCPInterface_AddUseCase_Call) RunAndReturn(run func()) *UCMGCPInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// CurrentPerPhase provides a mock function with given fields: entity +func (_m *UCMGCPInterface) CurrentPerPhase(entity api.EntityRemoteInterface) ([]float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for CurrentPerPhase") + } + + var r0 []float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) ([]float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) []float64); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]float64) + } + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCMGCPInterface_CurrentPerPhase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CurrentPerPhase' +type UCMGCPInterface_CurrentPerPhase_Call struct { + *mock.Call +} + +// CurrentPerPhase is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCMGCPInterface_Expecter) CurrentPerPhase(entity interface{}) *UCMGCPInterface_CurrentPerPhase_Call { + return &UCMGCPInterface_CurrentPerPhase_Call{Call: _e.mock.On("CurrentPerPhase", entity)} +} + +func (_c *UCMGCPInterface_CurrentPerPhase_Call) Run(run func(entity api.EntityRemoteInterface)) *UCMGCPInterface_CurrentPerPhase_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCMGCPInterface_CurrentPerPhase_Call) Return(_a0 []float64, _a1 error) *UCMGCPInterface_CurrentPerPhase_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCMGCPInterface_CurrentPerPhase_Call) RunAndReturn(run func(api.EntityRemoteInterface) ([]float64, error)) *UCMGCPInterface_CurrentPerPhase_Call { + _c.Call.Return(run) + return _c +} + +// EnergyConsumed provides a mock function with given fields: entity +func (_m *UCMGCPInterface) EnergyConsumed(entity api.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for EnergyConsumed") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCMGCPInterface_EnergyConsumed_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnergyConsumed' +type UCMGCPInterface_EnergyConsumed_Call struct { + *mock.Call +} + +// EnergyConsumed is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCMGCPInterface_Expecter) EnergyConsumed(entity interface{}) *UCMGCPInterface_EnergyConsumed_Call { + return &UCMGCPInterface_EnergyConsumed_Call{Call: _e.mock.On("EnergyConsumed", entity)} +} + +func (_c *UCMGCPInterface_EnergyConsumed_Call) Run(run func(entity api.EntityRemoteInterface)) *UCMGCPInterface_EnergyConsumed_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCMGCPInterface_EnergyConsumed_Call) Return(_a0 float64, _a1 error) *UCMGCPInterface_EnergyConsumed_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCMGCPInterface_EnergyConsumed_Call) RunAndReturn(run func(api.EntityRemoteInterface) (float64, error)) *UCMGCPInterface_EnergyConsumed_Call { + _c.Call.Return(run) + return _c +} + +// EnergyFeedIn provides a mock function with given fields: entity +func (_m *UCMGCPInterface) EnergyFeedIn(entity api.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for EnergyFeedIn") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCMGCPInterface_EnergyFeedIn_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnergyFeedIn' +type UCMGCPInterface_EnergyFeedIn_Call struct { + *mock.Call +} + +// EnergyFeedIn is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCMGCPInterface_Expecter) EnergyFeedIn(entity interface{}) *UCMGCPInterface_EnergyFeedIn_Call { + return &UCMGCPInterface_EnergyFeedIn_Call{Call: _e.mock.On("EnergyFeedIn", entity)} +} + +func (_c *UCMGCPInterface_EnergyFeedIn_Call) Run(run func(entity api.EntityRemoteInterface)) *UCMGCPInterface_EnergyFeedIn_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCMGCPInterface_EnergyFeedIn_Call) Return(_a0 float64, _a1 error) *UCMGCPInterface_EnergyFeedIn_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCMGCPInterface_EnergyFeedIn_Call) RunAndReturn(run func(api.EntityRemoteInterface) (float64, error)) *UCMGCPInterface_EnergyFeedIn_Call { + _c.Call.Return(run) + return _c +} + +// Frequency provides a mock function with given fields: entity +func (_m *UCMGCPInterface) Frequency(entity api.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for Frequency") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCMGCPInterface_Frequency_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Frequency' +type UCMGCPInterface_Frequency_Call struct { + *mock.Call +} + +// Frequency is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCMGCPInterface_Expecter) Frequency(entity interface{}) *UCMGCPInterface_Frequency_Call { + return &UCMGCPInterface_Frequency_Call{Call: _e.mock.On("Frequency", entity)} +} + +func (_c *UCMGCPInterface_Frequency_Call) Run(run func(entity api.EntityRemoteInterface)) *UCMGCPInterface_Frequency_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCMGCPInterface_Frequency_Call) Return(_a0 float64, _a1 error) *UCMGCPInterface_Frequency_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCMGCPInterface_Frequency_Call) RunAndReturn(run func(api.EntityRemoteInterface) (float64, error)) *UCMGCPInterface_Frequency_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *UCMGCPInterface) IsUseCaseSupported(remoteEntity api.EntityRemoteInterface) (bool, error) { + ret := _m.Called(remoteEntity) + + if len(ret) == 0 { + panic("no return value specified for IsUseCaseSupported") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCMGCPInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type UCMGCPInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity api.EntityRemoteInterface +func (_e *UCMGCPInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *UCMGCPInterface_IsUseCaseSupported_Call { + return &UCMGCPInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *UCMGCPInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity api.EntityRemoteInterface)) *UCMGCPInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCMGCPInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *UCMGCPInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCMGCPInterface_IsUseCaseSupported_Call) RunAndReturn(run func(api.EntityRemoteInterface) (bool, error)) *UCMGCPInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// Power provides a mock function with given fields: entity +func (_m *UCMGCPInterface) Power(entity api.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for Power") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCMGCPInterface_Power_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Power' +type UCMGCPInterface_Power_Call struct { + *mock.Call +} + +// Power is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCMGCPInterface_Expecter) Power(entity interface{}) *UCMGCPInterface_Power_Call { + return &UCMGCPInterface_Power_Call{Call: _e.mock.On("Power", entity)} +} + +func (_c *UCMGCPInterface_Power_Call) Run(run func(entity api.EntityRemoteInterface)) *UCMGCPInterface_Power_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCMGCPInterface_Power_Call) Return(_a0 float64, _a1 error) *UCMGCPInterface_Power_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCMGCPInterface_Power_Call) RunAndReturn(run func(api.EntityRemoteInterface) (float64, error)) *UCMGCPInterface_Power_Call { + _c.Call.Return(run) + return _c +} + +// PowerLimitationFactor provides a mock function with given fields: entity +func (_m *UCMGCPInterface) PowerLimitationFactor(entity api.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for PowerLimitationFactor") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCMGCPInterface_PowerLimitationFactor_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PowerLimitationFactor' +type UCMGCPInterface_PowerLimitationFactor_Call struct { + *mock.Call +} + +// PowerLimitationFactor is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCMGCPInterface_Expecter) PowerLimitationFactor(entity interface{}) *UCMGCPInterface_PowerLimitationFactor_Call { + return &UCMGCPInterface_PowerLimitationFactor_Call{Call: _e.mock.On("PowerLimitationFactor", entity)} +} + +func (_c *UCMGCPInterface_PowerLimitationFactor_Call) Run(run func(entity api.EntityRemoteInterface)) *UCMGCPInterface_PowerLimitationFactor_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCMGCPInterface_PowerLimitationFactor_Call) Return(_a0 float64, _a1 error) *UCMGCPInterface_PowerLimitationFactor_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCMGCPInterface_PowerLimitationFactor_Call) RunAndReturn(run func(api.EntityRemoteInterface) (float64, error)) *UCMGCPInterface_PowerLimitationFactor_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *UCMGCPInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// UCMGCPInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type UCMGCPInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *UCMGCPInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *UCMGCPInterface_UpdateUseCaseAvailability_Call { + return &UCMGCPInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *UCMGCPInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *UCMGCPInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *UCMGCPInterface_UpdateUseCaseAvailability_Call) Return() *UCMGCPInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *UCMGCPInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *UCMGCPInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// UseCaseName provides a mock function with given fields: +func (_m *UCMGCPInterface) UseCaseName() model.UseCaseNameType { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for UseCaseName") + } + + var r0 model.UseCaseNameType + if rf, ok := ret.Get(0).(func() model.UseCaseNameType); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(model.UseCaseNameType) + } + + return r0 +} + +// UCMGCPInterface_UseCaseName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UseCaseName' +type UCMGCPInterface_UseCaseName_Call struct { + *mock.Call +} + +// UseCaseName is a helper method to define mock.On call +func (_e *UCMGCPInterface_Expecter) UseCaseName() *UCMGCPInterface_UseCaseName_Call { + return &UCMGCPInterface_UseCaseName_Call{Call: _e.mock.On("UseCaseName")} +} + +func (_c *UCMGCPInterface_UseCaseName_Call) Run(run func()) *UCMGCPInterface_UseCaseName_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCMGCPInterface_UseCaseName_Call) Return(_a0 model.UseCaseNameType) *UCMGCPInterface_UseCaseName_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UCMGCPInterface_UseCaseName_Call) RunAndReturn(run func() model.UseCaseNameType) *UCMGCPInterface_UseCaseName_Call { + _c.Call.Return(run) + return _c +} + +// VoltagePerPhase provides a mock function with given fields: entity +func (_m *UCMGCPInterface) VoltagePerPhase(entity api.EntityRemoteInterface) ([]float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for VoltagePerPhase") + } + + var r0 []float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) ([]float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) []float64); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]float64) + } + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCMGCPInterface_VoltagePerPhase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VoltagePerPhase' +type UCMGCPInterface_VoltagePerPhase_Call struct { + *mock.Call +} + +// VoltagePerPhase is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCMGCPInterface_Expecter) VoltagePerPhase(entity interface{}) *UCMGCPInterface_VoltagePerPhase_Call { + return &UCMGCPInterface_VoltagePerPhase_Call{Call: _e.mock.On("VoltagePerPhase", entity)} +} + +func (_c *UCMGCPInterface_VoltagePerPhase_Call) Run(run func(entity api.EntityRemoteInterface)) *UCMGCPInterface_VoltagePerPhase_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCMGCPInterface_VoltagePerPhase_Call) Return(_a0 []float64, _a1 error) *UCMGCPInterface_VoltagePerPhase_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCMGCPInterface_VoltagePerPhase_Call) RunAndReturn(run func(api.EntityRemoteInterface) ([]float64, error)) *UCMGCPInterface_VoltagePerPhase_Call { + _c.Call.Return(run) + return _c +} + +// NewUCMGCPInterface creates a new instance of UCMGCPInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewUCMGCPInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *UCMGCPInterface { + mock := &UCMGCPInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/UCOPEVInterface.go b/mocks/UCOPEVInterface.go new file mode 100644 index 0000000..279f84c --- /dev/null +++ b/mocks/UCOPEVInterface.go @@ -0,0 +1,430 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + cemdapi "github.com/enbility/cemd/api" + api "github.com/enbility/spine-go/api" + + mock "github.com/stretchr/testify/mock" + + model "github.com/enbility/spine-go/model" +) + +// UCOPEVInterface is an autogenerated mock type for the UCOPEVInterface type +type UCOPEVInterface struct { + mock.Mock +} + +type UCOPEVInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *UCOPEVInterface) EXPECT() *UCOPEVInterface_Expecter { + return &UCOPEVInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *UCOPEVInterface) AddFeatures() { + _m.Called() +} + +// UCOPEVInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type UCOPEVInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *UCOPEVInterface_Expecter) AddFeatures() *UCOPEVInterface_AddFeatures_Call { + return &UCOPEVInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *UCOPEVInterface_AddFeatures_Call) Run(run func()) *UCOPEVInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCOPEVInterface_AddFeatures_Call) Return() *UCOPEVInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *UCOPEVInterface_AddFeatures_Call) RunAndReturn(run func()) *UCOPEVInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *UCOPEVInterface) AddUseCase() { + _m.Called() +} + +// UCOPEVInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type UCOPEVInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *UCOPEVInterface_Expecter) AddUseCase() *UCOPEVInterface_AddUseCase_Call { + return &UCOPEVInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *UCOPEVInterface_AddUseCase_Call) Run(run func()) *UCOPEVInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCOPEVInterface_AddUseCase_Call) Return() *UCOPEVInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *UCOPEVInterface_AddUseCase_Call) RunAndReturn(run func()) *UCOPEVInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// CurrentLimits provides a mock function with given fields: entity +func (_m *UCOPEVInterface) CurrentLimits(entity api.EntityRemoteInterface) ([]float64, []float64, []float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for CurrentLimits") + } + + var r0 []float64 + var r1 []float64 + var r2 []float64 + var r3 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) ([]float64, []float64, []float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) []float64); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]float64) + } + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) []float64); ok { + r1 = rf(entity) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).([]float64) + } + } + + if rf, ok := ret.Get(2).(func(api.EntityRemoteInterface) []float64); ok { + r2 = rf(entity) + } else { + if ret.Get(2) != nil { + r2 = ret.Get(2).([]float64) + } + } + + if rf, ok := ret.Get(3).(func(api.EntityRemoteInterface) error); ok { + r3 = rf(entity) + } else { + r3 = ret.Error(3) + } + + return r0, r1, r2, r3 +} + +// UCOPEVInterface_CurrentLimits_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CurrentLimits' +type UCOPEVInterface_CurrentLimits_Call struct { + *mock.Call +} + +// CurrentLimits is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCOPEVInterface_Expecter) CurrentLimits(entity interface{}) *UCOPEVInterface_CurrentLimits_Call { + return &UCOPEVInterface_CurrentLimits_Call{Call: _e.mock.On("CurrentLimits", entity)} +} + +func (_c *UCOPEVInterface_CurrentLimits_Call) Run(run func(entity api.EntityRemoteInterface)) *UCOPEVInterface_CurrentLimits_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCOPEVInterface_CurrentLimits_Call) Return(_a0 []float64, _a1 []float64, _a2 []float64, _a3 error) *UCOPEVInterface_CurrentLimits_Call { + _c.Call.Return(_a0, _a1, _a2, _a3) + return _c +} + +func (_c *UCOPEVInterface_CurrentLimits_Call) RunAndReturn(run func(api.EntityRemoteInterface) ([]float64, []float64, []float64, error)) *UCOPEVInterface_CurrentLimits_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *UCOPEVInterface) IsUseCaseSupported(remoteEntity api.EntityRemoteInterface) (bool, error) { + ret := _m.Called(remoteEntity) + + if len(ret) == 0 { + panic("no return value specified for IsUseCaseSupported") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCOPEVInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type UCOPEVInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity api.EntityRemoteInterface +func (_e *UCOPEVInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *UCOPEVInterface_IsUseCaseSupported_Call { + return &UCOPEVInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *UCOPEVInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity api.EntityRemoteInterface)) *UCOPEVInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCOPEVInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *UCOPEVInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCOPEVInterface_IsUseCaseSupported_Call) RunAndReturn(run func(api.EntityRemoteInterface) (bool, error)) *UCOPEVInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// LoadControlLimits provides a mock function with given fields: entity +func (_m *UCOPEVInterface) LoadControlLimits(entity api.EntityRemoteInterface) ([]cemdapi.LoadLimitsPhase, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for LoadControlLimits") + } + + var r0 []cemdapi.LoadLimitsPhase + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) ([]cemdapi.LoadLimitsPhase, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) []cemdapi.LoadLimitsPhase); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]cemdapi.LoadLimitsPhase) + } + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCOPEVInterface_LoadControlLimits_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LoadControlLimits' +type UCOPEVInterface_LoadControlLimits_Call struct { + *mock.Call +} + +// LoadControlLimits is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCOPEVInterface_Expecter) LoadControlLimits(entity interface{}) *UCOPEVInterface_LoadControlLimits_Call { + return &UCOPEVInterface_LoadControlLimits_Call{Call: _e.mock.On("LoadControlLimits", entity)} +} + +func (_c *UCOPEVInterface_LoadControlLimits_Call) Run(run func(entity api.EntityRemoteInterface)) *UCOPEVInterface_LoadControlLimits_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCOPEVInterface_LoadControlLimits_Call) Return(limits []cemdapi.LoadLimitsPhase, resultErr error) *UCOPEVInterface_LoadControlLimits_Call { + _c.Call.Return(limits, resultErr) + return _c +} + +func (_c *UCOPEVInterface_LoadControlLimits_Call) RunAndReturn(run func(api.EntityRemoteInterface) ([]cemdapi.LoadLimitsPhase, error)) *UCOPEVInterface_LoadControlLimits_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *UCOPEVInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// UCOPEVInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type UCOPEVInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *UCOPEVInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *UCOPEVInterface_UpdateUseCaseAvailability_Call { + return &UCOPEVInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *UCOPEVInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *UCOPEVInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *UCOPEVInterface_UpdateUseCaseAvailability_Call) Return() *UCOPEVInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *UCOPEVInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *UCOPEVInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// UseCaseName provides a mock function with given fields: +func (_m *UCOPEVInterface) UseCaseName() model.UseCaseNameType { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for UseCaseName") + } + + var r0 model.UseCaseNameType + if rf, ok := ret.Get(0).(func() model.UseCaseNameType); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(model.UseCaseNameType) + } + + return r0 +} + +// UCOPEVInterface_UseCaseName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UseCaseName' +type UCOPEVInterface_UseCaseName_Call struct { + *mock.Call +} + +// UseCaseName is a helper method to define mock.On call +func (_e *UCOPEVInterface_Expecter) UseCaseName() *UCOPEVInterface_UseCaseName_Call { + return &UCOPEVInterface_UseCaseName_Call{Call: _e.mock.On("UseCaseName")} +} + +func (_c *UCOPEVInterface_UseCaseName_Call) Run(run func()) *UCOPEVInterface_UseCaseName_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCOPEVInterface_UseCaseName_Call) Return(_a0 model.UseCaseNameType) *UCOPEVInterface_UseCaseName_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UCOPEVInterface_UseCaseName_Call) RunAndReturn(run func() model.UseCaseNameType) *UCOPEVInterface_UseCaseName_Call { + _c.Call.Return(run) + return _c +} + +// WriteLoadControlLimits provides a mock function with given fields: entity, limits +func (_m *UCOPEVInterface) WriteLoadControlLimits(entity api.EntityRemoteInterface, limits []cemdapi.LoadLimitsPhase) (*model.MsgCounterType, error) { + ret := _m.Called(entity, limits) + + if len(ret) == 0 { + panic("no return value specified for WriteLoadControlLimits") + } + + var r0 *model.MsgCounterType + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface, []cemdapi.LoadLimitsPhase) (*model.MsgCounterType, error)); ok { + return rf(entity, limits) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface, []cemdapi.LoadLimitsPhase) *model.MsgCounterType); ok { + r0 = rf(entity, limits) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.MsgCounterType) + } + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface, []cemdapi.LoadLimitsPhase) error); ok { + r1 = rf(entity, limits) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCOPEVInterface_WriteLoadControlLimits_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteLoadControlLimits' +type UCOPEVInterface_WriteLoadControlLimits_Call struct { + *mock.Call +} + +// WriteLoadControlLimits is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +// - limits []cemdapi.LoadLimitsPhase +func (_e *UCOPEVInterface_Expecter) WriteLoadControlLimits(entity interface{}, limits interface{}) *UCOPEVInterface_WriteLoadControlLimits_Call { + return &UCOPEVInterface_WriteLoadControlLimits_Call{Call: _e.mock.On("WriteLoadControlLimits", entity, limits)} +} + +func (_c *UCOPEVInterface_WriteLoadControlLimits_Call) Run(run func(entity api.EntityRemoteInterface, limits []cemdapi.LoadLimitsPhase)) *UCOPEVInterface_WriteLoadControlLimits_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface), args[1].([]cemdapi.LoadLimitsPhase)) + }) + return _c +} + +func (_c *UCOPEVInterface_WriteLoadControlLimits_Call) Return(_a0 *model.MsgCounterType, _a1 error) *UCOPEVInterface_WriteLoadControlLimits_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCOPEVInterface_WriteLoadControlLimits_Call) RunAndReturn(run func(api.EntityRemoteInterface, []cemdapi.LoadLimitsPhase) (*model.MsgCounterType, error)) *UCOPEVInterface_WriteLoadControlLimits_Call { + _c.Call.Return(run) + return _c +} + +// NewUCOPEVInterface creates a new instance of UCOPEVInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewUCOPEVInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *UCOPEVInterface { + mock := &UCOPEVInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/UCOSCEVInterface.go b/mocks/UCOSCEVInterface.go new file mode 100644 index 0000000..3ede470 --- /dev/null +++ b/mocks/UCOSCEVInterface.go @@ -0,0 +1,430 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + cemdapi "github.com/enbility/cemd/api" + api "github.com/enbility/spine-go/api" + + mock "github.com/stretchr/testify/mock" + + model "github.com/enbility/spine-go/model" +) + +// UCOSCEVInterface is an autogenerated mock type for the UCOSCEVInterface type +type UCOSCEVInterface struct { + mock.Mock +} + +type UCOSCEVInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *UCOSCEVInterface) EXPECT() *UCOSCEVInterface_Expecter { + return &UCOSCEVInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *UCOSCEVInterface) AddFeatures() { + _m.Called() +} + +// UCOSCEVInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type UCOSCEVInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *UCOSCEVInterface_Expecter) AddFeatures() *UCOSCEVInterface_AddFeatures_Call { + return &UCOSCEVInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *UCOSCEVInterface_AddFeatures_Call) Run(run func()) *UCOSCEVInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCOSCEVInterface_AddFeatures_Call) Return() *UCOSCEVInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *UCOSCEVInterface_AddFeatures_Call) RunAndReturn(run func()) *UCOSCEVInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *UCOSCEVInterface) AddUseCase() { + _m.Called() +} + +// UCOSCEVInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type UCOSCEVInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *UCOSCEVInterface_Expecter) AddUseCase() *UCOSCEVInterface_AddUseCase_Call { + return &UCOSCEVInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *UCOSCEVInterface_AddUseCase_Call) Run(run func()) *UCOSCEVInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCOSCEVInterface_AddUseCase_Call) Return() *UCOSCEVInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *UCOSCEVInterface_AddUseCase_Call) RunAndReturn(run func()) *UCOSCEVInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// CurrentLimits provides a mock function with given fields: entity +func (_m *UCOSCEVInterface) CurrentLimits(entity api.EntityRemoteInterface) ([]float64, []float64, []float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for CurrentLimits") + } + + var r0 []float64 + var r1 []float64 + var r2 []float64 + var r3 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) ([]float64, []float64, []float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) []float64); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]float64) + } + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) []float64); ok { + r1 = rf(entity) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).([]float64) + } + } + + if rf, ok := ret.Get(2).(func(api.EntityRemoteInterface) []float64); ok { + r2 = rf(entity) + } else { + if ret.Get(2) != nil { + r2 = ret.Get(2).([]float64) + } + } + + if rf, ok := ret.Get(3).(func(api.EntityRemoteInterface) error); ok { + r3 = rf(entity) + } else { + r3 = ret.Error(3) + } + + return r0, r1, r2, r3 +} + +// UCOSCEVInterface_CurrentLimits_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CurrentLimits' +type UCOSCEVInterface_CurrentLimits_Call struct { + *mock.Call +} + +// CurrentLimits is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCOSCEVInterface_Expecter) CurrentLimits(entity interface{}) *UCOSCEVInterface_CurrentLimits_Call { + return &UCOSCEVInterface_CurrentLimits_Call{Call: _e.mock.On("CurrentLimits", entity)} +} + +func (_c *UCOSCEVInterface_CurrentLimits_Call) Run(run func(entity api.EntityRemoteInterface)) *UCOSCEVInterface_CurrentLimits_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCOSCEVInterface_CurrentLimits_Call) Return(_a0 []float64, _a1 []float64, _a2 []float64, _a3 error) *UCOSCEVInterface_CurrentLimits_Call { + _c.Call.Return(_a0, _a1, _a2, _a3) + return _c +} + +func (_c *UCOSCEVInterface_CurrentLimits_Call) RunAndReturn(run func(api.EntityRemoteInterface) ([]float64, []float64, []float64, error)) *UCOSCEVInterface_CurrentLimits_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *UCOSCEVInterface) IsUseCaseSupported(remoteEntity api.EntityRemoteInterface) (bool, error) { + ret := _m.Called(remoteEntity) + + if len(ret) == 0 { + panic("no return value specified for IsUseCaseSupported") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCOSCEVInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type UCOSCEVInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity api.EntityRemoteInterface +func (_e *UCOSCEVInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *UCOSCEVInterface_IsUseCaseSupported_Call { + return &UCOSCEVInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *UCOSCEVInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity api.EntityRemoteInterface)) *UCOSCEVInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCOSCEVInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *UCOSCEVInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCOSCEVInterface_IsUseCaseSupported_Call) RunAndReturn(run func(api.EntityRemoteInterface) (bool, error)) *UCOSCEVInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// LoadControlLimits provides a mock function with given fields: entity +func (_m *UCOSCEVInterface) LoadControlLimits(entity api.EntityRemoteInterface) ([]cemdapi.LoadLimitsPhase, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for LoadControlLimits") + } + + var r0 []cemdapi.LoadLimitsPhase + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) ([]cemdapi.LoadLimitsPhase, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) []cemdapi.LoadLimitsPhase); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]cemdapi.LoadLimitsPhase) + } + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCOSCEVInterface_LoadControlLimits_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LoadControlLimits' +type UCOSCEVInterface_LoadControlLimits_Call struct { + *mock.Call +} + +// LoadControlLimits is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCOSCEVInterface_Expecter) LoadControlLimits(entity interface{}) *UCOSCEVInterface_LoadControlLimits_Call { + return &UCOSCEVInterface_LoadControlLimits_Call{Call: _e.mock.On("LoadControlLimits", entity)} +} + +func (_c *UCOSCEVInterface_LoadControlLimits_Call) Run(run func(entity api.EntityRemoteInterface)) *UCOSCEVInterface_LoadControlLimits_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCOSCEVInterface_LoadControlLimits_Call) Return(limits []cemdapi.LoadLimitsPhase, resultErr error) *UCOSCEVInterface_LoadControlLimits_Call { + _c.Call.Return(limits, resultErr) + return _c +} + +func (_c *UCOSCEVInterface_LoadControlLimits_Call) RunAndReturn(run func(api.EntityRemoteInterface) ([]cemdapi.LoadLimitsPhase, error)) *UCOSCEVInterface_LoadControlLimits_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *UCOSCEVInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// UCOSCEVInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type UCOSCEVInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *UCOSCEVInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *UCOSCEVInterface_UpdateUseCaseAvailability_Call { + return &UCOSCEVInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *UCOSCEVInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *UCOSCEVInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *UCOSCEVInterface_UpdateUseCaseAvailability_Call) Return() *UCOSCEVInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *UCOSCEVInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *UCOSCEVInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// UseCaseName provides a mock function with given fields: +func (_m *UCOSCEVInterface) UseCaseName() model.UseCaseNameType { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for UseCaseName") + } + + var r0 model.UseCaseNameType + if rf, ok := ret.Get(0).(func() model.UseCaseNameType); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(model.UseCaseNameType) + } + + return r0 +} + +// UCOSCEVInterface_UseCaseName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UseCaseName' +type UCOSCEVInterface_UseCaseName_Call struct { + *mock.Call +} + +// UseCaseName is a helper method to define mock.On call +func (_e *UCOSCEVInterface_Expecter) UseCaseName() *UCOSCEVInterface_UseCaseName_Call { + return &UCOSCEVInterface_UseCaseName_Call{Call: _e.mock.On("UseCaseName")} +} + +func (_c *UCOSCEVInterface_UseCaseName_Call) Run(run func()) *UCOSCEVInterface_UseCaseName_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCOSCEVInterface_UseCaseName_Call) Return(_a0 model.UseCaseNameType) *UCOSCEVInterface_UseCaseName_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UCOSCEVInterface_UseCaseName_Call) RunAndReturn(run func() model.UseCaseNameType) *UCOSCEVInterface_UseCaseName_Call { + _c.Call.Return(run) + return _c +} + +// WriteLoadControlLimits provides a mock function with given fields: entity, limits +func (_m *UCOSCEVInterface) WriteLoadControlLimits(entity api.EntityRemoteInterface, limits []cemdapi.LoadLimitsPhase) (*model.MsgCounterType, error) { + ret := _m.Called(entity, limits) + + if len(ret) == 0 { + panic("no return value specified for WriteLoadControlLimits") + } + + var r0 *model.MsgCounterType + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface, []cemdapi.LoadLimitsPhase) (*model.MsgCounterType, error)); ok { + return rf(entity, limits) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface, []cemdapi.LoadLimitsPhase) *model.MsgCounterType); ok { + r0 = rf(entity, limits) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.MsgCounterType) + } + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface, []cemdapi.LoadLimitsPhase) error); ok { + r1 = rf(entity, limits) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCOSCEVInterface_WriteLoadControlLimits_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteLoadControlLimits' +type UCOSCEVInterface_WriteLoadControlLimits_Call struct { + *mock.Call +} + +// WriteLoadControlLimits is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +// - limits []cemdapi.LoadLimitsPhase +func (_e *UCOSCEVInterface_Expecter) WriteLoadControlLimits(entity interface{}, limits interface{}) *UCOSCEVInterface_WriteLoadControlLimits_Call { + return &UCOSCEVInterface_WriteLoadControlLimits_Call{Call: _e.mock.On("WriteLoadControlLimits", entity, limits)} +} + +func (_c *UCOSCEVInterface_WriteLoadControlLimits_Call) Run(run func(entity api.EntityRemoteInterface, limits []cemdapi.LoadLimitsPhase)) *UCOSCEVInterface_WriteLoadControlLimits_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface), args[1].([]cemdapi.LoadLimitsPhase)) + }) + return _c +} + +func (_c *UCOSCEVInterface_WriteLoadControlLimits_Call) Return(_a0 *model.MsgCounterType, _a1 error) *UCOSCEVInterface_WriteLoadControlLimits_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCOSCEVInterface_WriteLoadControlLimits_Call) RunAndReturn(run func(api.EntityRemoteInterface, []cemdapi.LoadLimitsPhase) (*model.MsgCounterType, error)) *UCOSCEVInterface_WriteLoadControlLimits_Call { + _c.Call.Return(run) + return _c +} + +// NewUCOSCEVInterface creates a new instance of UCOSCEVInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewUCOSCEVInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *UCOSCEVInterface { + mock := &UCOSCEVInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/UCVABDInterface.go b/mocks/UCVABDInterface.go new file mode 100644 index 0000000..d761012 --- /dev/null +++ b/mocks/UCVABDInterface.go @@ -0,0 +1,459 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + api "github.com/enbility/spine-go/api" + mock "github.com/stretchr/testify/mock" + + model "github.com/enbility/spine-go/model" +) + +// UCVABDInterface is an autogenerated mock type for the UCVABDInterface type +type UCVABDInterface struct { + mock.Mock +} + +type UCVABDInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *UCVABDInterface) EXPECT() *UCVABDInterface_Expecter { + return &UCVABDInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *UCVABDInterface) AddFeatures() { + _m.Called() +} + +// UCVABDInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type UCVABDInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *UCVABDInterface_Expecter) AddFeatures() *UCVABDInterface_AddFeatures_Call { + return &UCVABDInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *UCVABDInterface_AddFeatures_Call) Run(run func()) *UCVABDInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCVABDInterface_AddFeatures_Call) Return() *UCVABDInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *UCVABDInterface_AddFeatures_Call) RunAndReturn(run func()) *UCVABDInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *UCVABDInterface) AddUseCase() { + _m.Called() +} + +// UCVABDInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type UCVABDInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *UCVABDInterface_Expecter) AddUseCase() *UCVABDInterface_AddUseCase_Call { + return &UCVABDInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *UCVABDInterface_AddUseCase_Call) Run(run func()) *UCVABDInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCVABDInterface_AddUseCase_Call) Return() *UCVABDInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *UCVABDInterface_AddUseCase_Call) RunAndReturn(run func()) *UCVABDInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// EnergyCharged provides a mock function with given fields: entity +func (_m *UCVABDInterface) EnergyCharged(entity api.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for EnergyCharged") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCVABDInterface_EnergyCharged_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnergyCharged' +type UCVABDInterface_EnergyCharged_Call struct { + *mock.Call +} + +// EnergyCharged is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCVABDInterface_Expecter) EnergyCharged(entity interface{}) *UCVABDInterface_EnergyCharged_Call { + return &UCVABDInterface_EnergyCharged_Call{Call: _e.mock.On("EnergyCharged", entity)} +} + +func (_c *UCVABDInterface_EnergyCharged_Call) Run(run func(entity api.EntityRemoteInterface)) *UCVABDInterface_EnergyCharged_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCVABDInterface_EnergyCharged_Call) Return(_a0 float64, _a1 error) *UCVABDInterface_EnergyCharged_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCVABDInterface_EnergyCharged_Call) RunAndReturn(run func(api.EntityRemoteInterface) (float64, error)) *UCVABDInterface_EnergyCharged_Call { + _c.Call.Return(run) + return _c +} + +// EnergyDischarged provides a mock function with given fields: entity +func (_m *UCVABDInterface) EnergyDischarged(entity api.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for EnergyDischarged") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCVABDInterface_EnergyDischarged_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnergyDischarged' +type UCVABDInterface_EnergyDischarged_Call struct { + *mock.Call +} + +// EnergyDischarged is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCVABDInterface_Expecter) EnergyDischarged(entity interface{}) *UCVABDInterface_EnergyDischarged_Call { + return &UCVABDInterface_EnergyDischarged_Call{Call: _e.mock.On("EnergyDischarged", entity)} +} + +func (_c *UCVABDInterface_EnergyDischarged_Call) Run(run func(entity api.EntityRemoteInterface)) *UCVABDInterface_EnergyDischarged_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCVABDInterface_EnergyDischarged_Call) Return(_a0 float64, _a1 error) *UCVABDInterface_EnergyDischarged_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCVABDInterface_EnergyDischarged_Call) RunAndReturn(run func(api.EntityRemoteInterface) (float64, error)) *UCVABDInterface_EnergyDischarged_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *UCVABDInterface) IsUseCaseSupported(remoteEntity api.EntityRemoteInterface) (bool, error) { + ret := _m.Called(remoteEntity) + + if len(ret) == 0 { + panic("no return value specified for IsUseCaseSupported") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCVABDInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type UCVABDInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity api.EntityRemoteInterface +func (_e *UCVABDInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *UCVABDInterface_IsUseCaseSupported_Call { + return &UCVABDInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *UCVABDInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity api.EntityRemoteInterface)) *UCVABDInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCVABDInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *UCVABDInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCVABDInterface_IsUseCaseSupported_Call) RunAndReturn(run func(api.EntityRemoteInterface) (bool, error)) *UCVABDInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// Power provides a mock function with given fields: entity +func (_m *UCVABDInterface) Power(entity api.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for Power") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCVABDInterface_Power_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Power' +type UCVABDInterface_Power_Call struct { + *mock.Call +} + +// Power is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCVABDInterface_Expecter) Power(entity interface{}) *UCVABDInterface_Power_Call { + return &UCVABDInterface_Power_Call{Call: _e.mock.On("Power", entity)} +} + +func (_c *UCVABDInterface_Power_Call) Run(run func(entity api.EntityRemoteInterface)) *UCVABDInterface_Power_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCVABDInterface_Power_Call) Return(_a0 float64, _a1 error) *UCVABDInterface_Power_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCVABDInterface_Power_Call) RunAndReturn(run func(api.EntityRemoteInterface) (float64, error)) *UCVABDInterface_Power_Call { + _c.Call.Return(run) + return _c +} + +// StateOfCharge provides a mock function with given fields: entity +func (_m *UCVABDInterface) StateOfCharge(entity api.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for StateOfCharge") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCVABDInterface_StateOfCharge_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StateOfCharge' +type UCVABDInterface_StateOfCharge_Call struct { + *mock.Call +} + +// StateOfCharge is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCVABDInterface_Expecter) StateOfCharge(entity interface{}) *UCVABDInterface_StateOfCharge_Call { + return &UCVABDInterface_StateOfCharge_Call{Call: _e.mock.On("StateOfCharge", entity)} +} + +func (_c *UCVABDInterface_StateOfCharge_Call) Run(run func(entity api.EntityRemoteInterface)) *UCVABDInterface_StateOfCharge_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCVABDInterface_StateOfCharge_Call) Return(_a0 float64, _a1 error) *UCVABDInterface_StateOfCharge_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCVABDInterface_StateOfCharge_Call) RunAndReturn(run func(api.EntityRemoteInterface) (float64, error)) *UCVABDInterface_StateOfCharge_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *UCVABDInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// UCVABDInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type UCVABDInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *UCVABDInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *UCVABDInterface_UpdateUseCaseAvailability_Call { + return &UCVABDInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *UCVABDInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *UCVABDInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *UCVABDInterface_UpdateUseCaseAvailability_Call) Return() *UCVABDInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *UCVABDInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *UCVABDInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// UseCaseName provides a mock function with given fields: +func (_m *UCVABDInterface) UseCaseName() model.UseCaseNameType { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for UseCaseName") + } + + var r0 model.UseCaseNameType + if rf, ok := ret.Get(0).(func() model.UseCaseNameType); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(model.UseCaseNameType) + } + + return r0 +} + +// UCVABDInterface_UseCaseName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UseCaseName' +type UCVABDInterface_UseCaseName_Call struct { + *mock.Call +} + +// UseCaseName is a helper method to define mock.On call +func (_e *UCVABDInterface_Expecter) UseCaseName() *UCVABDInterface_UseCaseName_Call { + return &UCVABDInterface_UseCaseName_Call{Call: _e.mock.On("UseCaseName")} +} + +func (_c *UCVABDInterface_UseCaseName_Call) Run(run func()) *UCVABDInterface_UseCaseName_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCVABDInterface_UseCaseName_Call) Return(_a0 model.UseCaseNameType) *UCVABDInterface_UseCaseName_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UCVABDInterface_UseCaseName_Call) RunAndReturn(run func() model.UseCaseNameType) *UCVABDInterface_UseCaseName_Call { + _c.Call.Return(run) + return _c +} + +// NewUCVABDInterface creates a new instance of UCVABDInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewUCVABDInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *UCVABDInterface { + mock := &UCVABDInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/UCVAPDInterface.go b/mocks/UCVAPDInterface.go new file mode 100644 index 0000000..e2ee134 --- /dev/null +++ b/mocks/UCVAPDInterface.go @@ -0,0 +1,403 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + api "github.com/enbility/spine-go/api" + mock "github.com/stretchr/testify/mock" + + model "github.com/enbility/spine-go/model" +) + +// UCVAPDInterface is an autogenerated mock type for the UCVAPDInterface type +type UCVAPDInterface struct { + mock.Mock +} + +type UCVAPDInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *UCVAPDInterface) EXPECT() *UCVAPDInterface_Expecter { + return &UCVAPDInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *UCVAPDInterface) AddFeatures() { + _m.Called() +} + +// UCVAPDInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type UCVAPDInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *UCVAPDInterface_Expecter) AddFeatures() *UCVAPDInterface_AddFeatures_Call { + return &UCVAPDInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *UCVAPDInterface_AddFeatures_Call) Run(run func()) *UCVAPDInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCVAPDInterface_AddFeatures_Call) Return() *UCVAPDInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *UCVAPDInterface_AddFeatures_Call) RunAndReturn(run func()) *UCVAPDInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *UCVAPDInterface) AddUseCase() { + _m.Called() +} + +// UCVAPDInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type UCVAPDInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *UCVAPDInterface_Expecter) AddUseCase() *UCVAPDInterface_AddUseCase_Call { + return &UCVAPDInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *UCVAPDInterface_AddUseCase_Call) Run(run func()) *UCVAPDInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCVAPDInterface_AddUseCase_Call) Return() *UCVAPDInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *UCVAPDInterface_AddUseCase_Call) RunAndReturn(run func()) *UCVAPDInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *UCVAPDInterface) IsUseCaseSupported(remoteEntity api.EntityRemoteInterface) (bool, error) { + ret := _m.Called(remoteEntity) + + if len(ret) == 0 { + panic("no return value specified for IsUseCaseSupported") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCVAPDInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type UCVAPDInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity api.EntityRemoteInterface +func (_e *UCVAPDInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *UCVAPDInterface_IsUseCaseSupported_Call { + return &UCVAPDInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *UCVAPDInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity api.EntityRemoteInterface)) *UCVAPDInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCVAPDInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *UCVAPDInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCVAPDInterface_IsUseCaseSupported_Call) RunAndReturn(run func(api.EntityRemoteInterface) (bool, error)) *UCVAPDInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// PVYieldTotal provides a mock function with given fields: entity +func (_m *UCVAPDInterface) PVYieldTotal(entity api.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for PVYieldTotal") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCVAPDInterface_PVYieldTotal_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PVYieldTotal' +type UCVAPDInterface_PVYieldTotal_Call struct { + *mock.Call +} + +// PVYieldTotal is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCVAPDInterface_Expecter) PVYieldTotal(entity interface{}) *UCVAPDInterface_PVYieldTotal_Call { + return &UCVAPDInterface_PVYieldTotal_Call{Call: _e.mock.On("PVYieldTotal", entity)} +} + +func (_c *UCVAPDInterface_PVYieldTotal_Call) Run(run func(entity api.EntityRemoteInterface)) *UCVAPDInterface_PVYieldTotal_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCVAPDInterface_PVYieldTotal_Call) Return(_a0 float64, _a1 error) *UCVAPDInterface_PVYieldTotal_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCVAPDInterface_PVYieldTotal_Call) RunAndReturn(run func(api.EntityRemoteInterface) (float64, error)) *UCVAPDInterface_PVYieldTotal_Call { + _c.Call.Return(run) + return _c +} + +// Power provides a mock function with given fields: entity +func (_m *UCVAPDInterface) Power(entity api.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for Power") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCVAPDInterface_Power_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Power' +type UCVAPDInterface_Power_Call struct { + *mock.Call +} + +// Power is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCVAPDInterface_Expecter) Power(entity interface{}) *UCVAPDInterface_Power_Call { + return &UCVAPDInterface_Power_Call{Call: _e.mock.On("Power", entity)} +} + +func (_c *UCVAPDInterface_Power_Call) Run(run func(entity api.EntityRemoteInterface)) *UCVAPDInterface_Power_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCVAPDInterface_Power_Call) Return(_a0 float64, _a1 error) *UCVAPDInterface_Power_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCVAPDInterface_Power_Call) RunAndReturn(run func(api.EntityRemoteInterface) (float64, error)) *UCVAPDInterface_Power_Call { + _c.Call.Return(run) + return _c +} + +// PowerNominalPeak provides a mock function with given fields: entity +func (_m *UCVAPDInterface) PowerNominalPeak(entity api.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for PowerNominalPeak") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCVAPDInterface_PowerNominalPeak_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PowerNominalPeak' +type UCVAPDInterface_PowerNominalPeak_Call struct { + *mock.Call +} + +// PowerNominalPeak is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCVAPDInterface_Expecter) PowerNominalPeak(entity interface{}) *UCVAPDInterface_PowerNominalPeak_Call { + return &UCVAPDInterface_PowerNominalPeak_Call{Call: _e.mock.On("PowerNominalPeak", entity)} +} + +func (_c *UCVAPDInterface_PowerNominalPeak_Call) Run(run func(entity api.EntityRemoteInterface)) *UCVAPDInterface_PowerNominalPeak_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCVAPDInterface_PowerNominalPeak_Call) Return(_a0 float64, _a1 error) *UCVAPDInterface_PowerNominalPeak_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCVAPDInterface_PowerNominalPeak_Call) RunAndReturn(run func(api.EntityRemoteInterface) (float64, error)) *UCVAPDInterface_PowerNominalPeak_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *UCVAPDInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// UCVAPDInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type UCVAPDInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *UCVAPDInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *UCVAPDInterface_UpdateUseCaseAvailability_Call { + return &UCVAPDInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *UCVAPDInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *UCVAPDInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *UCVAPDInterface_UpdateUseCaseAvailability_Call) Return() *UCVAPDInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *UCVAPDInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *UCVAPDInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// UseCaseName provides a mock function with given fields: +func (_m *UCVAPDInterface) UseCaseName() model.UseCaseNameType { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for UseCaseName") + } + + var r0 model.UseCaseNameType + if rf, ok := ret.Get(0).(func() model.UseCaseNameType); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(model.UseCaseNameType) + } + + return r0 +} + +// UCVAPDInterface_UseCaseName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UseCaseName' +type UCVAPDInterface_UseCaseName_Call struct { + *mock.Call +} + +// UseCaseName is a helper method to define mock.On call +func (_e *UCVAPDInterface_Expecter) UseCaseName() *UCVAPDInterface_UseCaseName_Call { + return &UCVAPDInterface_UseCaseName_Call{Call: _e.mock.On("UseCaseName")} +} + +func (_c *UCVAPDInterface_UseCaseName_Call) Run(run func()) *UCVAPDInterface_UseCaseName_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCVAPDInterface_UseCaseName_Call) Return(_a0 model.UseCaseNameType) *UCVAPDInterface_UseCaseName_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UCVAPDInterface_UseCaseName_Call) RunAndReturn(run func() model.UseCaseNameType) *UCVAPDInterface_UseCaseName_Call { + _c.Call.Return(run) + return _c +} + +// NewUCVAPDInterface creates a new instance of UCVAPDInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewUCVAPDInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *UCVAPDInterface { + mock := &UCVAPDInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/UseCaseInterface.go b/mocks/UseCaseInterface.go new file mode 100644 index 0000000..98831d8 --- /dev/null +++ b/mocks/UseCaseInterface.go @@ -0,0 +1,235 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + model "github.com/enbility/spine-go/model" + mock "github.com/stretchr/testify/mock" + + spine_goapi "github.com/enbility/spine-go/api" +) + +// UseCaseInterface is an autogenerated mock type for the UseCaseInterface type +type UseCaseInterface struct { + mock.Mock +} + +type UseCaseInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *UseCaseInterface) EXPECT() *UseCaseInterface_Expecter { + return &UseCaseInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *UseCaseInterface) AddFeatures() { + _m.Called() +} + +// UseCaseInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type UseCaseInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *UseCaseInterface_Expecter) AddFeatures() *UseCaseInterface_AddFeatures_Call { + return &UseCaseInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *UseCaseInterface_AddFeatures_Call) Run(run func()) *UseCaseInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UseCaseInterface_AddFeatures_Call) Return() *UseCaseInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *UseCaseInterface_AddFeatures_Call) RunAndReturn(run func()) *UseCaseInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *UseCaseInterface) AddUseCase() { + _m.Called() +} + +// UseCaseInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type UseCaseInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *UseCaseInterface_Expecter) AddUseCase() *UseCaseInterface_AddUseCase_Call { + return &UseCaseInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *UseCaseInterface_AddUseCase_Call) Run(run func()) *UseCaseInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UseCaseInterface_AddUseCase_Call) Return() *UseCaseInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *UseCaseInterface_AddUseCase_Call) RunAndReturn(run func()) *UseCaseInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *UseCaseInterface) IsUseCaseSupported(remoteEntity spine_goapi.EntityRemoteInterface) (bool, error) { + ret := _m.Called(remoteEntity) + + if len(ret) == 0 { + panic("no return value specified for IsUseCaseSupported") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UseCaseInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type UseCaseInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity spine_goapi.EntityRemoteInterface +func (_e *UseCaseInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *UseCaseInterface_IsUseCaseSupported_Call { + return &UseCaseInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *UseCaseInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity spine_goapi.EntityRemoteInterface)) *UseCaseInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UseCaseInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *UseCaseInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UseCaseInterface_IsUseCaseSupported_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (bool, error)) *UseCaseInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *UseCaseInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// UseCaseInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type UseCaseInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *UseCaseInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *UseCaseInterface_UpdateUseCaseAvailability_Call { + return &UseCaseInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *UseCaseInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *UseCaseInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *UseCaseInterface_UpdateUseCaseAvailability_Call) Return() *UseCaseInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *UseCaseInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *UseCaseInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// UseCaseName provides a mock function with given fields: +func (_m *UseCaseInterface) UseCaseName() model.UseCaseNameType { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for UseCaseName") + } + + var r0 model.UseCaseNameType + if rf, ok := ret.Get(0).(func() model.UseCaseNameType); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(model.UseCaseNameType) + } + + return r0 +} + +// UseCaseInterface_UseCaseName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UseCaseName' +type UseCaseInterface_UseCaseName_Call struct { + *mock.Call +} + +// UseCaseName is a helper method to define mock.On call +func (_e *UseCaseInterface_Expecter) UseCaseName() *UseCaseInterface_UseCaseName_Call { + return &UseCaseInterface_UseCaseName_Call{Call: _e.mock.On("UseCaseName")} +} + +func (_c *UseCaseInterface_UseCaseName_Call) Run(run func()) *UseCaseInterface_UseCaseName_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UseCaseInterface_UseCaseName_Call) Return(_a0 model.UseCaseNameType) *UseCaseInterface_UseCaseName_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UseCaseInterface_UseCaseName_Call) RunAndReturn(run func() model.UseCaseNameType) *UseCaseInterface_UseCaseName_Call { + _c.Call.Return(run) + return _c +} + +// NewUseCaseInterface creates a new instance of UseCaseInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewUseCaseInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *UseCaseInterface { + mock := &UseCaseInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/scenarios/types.go b/scenarios/types.go deleted file mode 100644 index ecc9a5f..0000000 --- a/scenarios/types.go +++ /dev/null @@ -1,23 +0,0 @@ -package scenarios - -import ( - "github.com/enbility/eebus-go/service" -) - -// Implemented by *ScenarioImpl, used by CemImpl -type ScenariosI interface { - RegisterRemoteDevice(details *service.ServiceDetails, dataProvider any) any - UnRegisterRemoteDevice(remoteDeviceSki string) error - AddFeatures() - AddUseCases() -} - -type ScenarioImpl struct { - Service *service.EEBUSService -} - -func NewScenarioImpl(service *service.EEBUSService) *ScenarioImpl { - return &ScenarioImpl{ - Service: service, - } -} diff --git a/uccevc/api.go b/uccevc/api.go new file mode 100644 index 0000000..7c8f665 --- /dev/null +++ b/uccevc/api.go @@ -0,0 +1,92 @@ +package uccevc + +import ( + "github.com/enbility/cemd/api" + spineapi "github.com/enbility/spine-go/api" +) + +//go:generate mockery + +// interface for the Coordinated EV Charging UseCase +type UCCEVCInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // returns the current charging stratey + // + // parameters: + // - entity: the entity of the EV + // + // 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 + ChargeStrategy(remoteEntity spineapi.EntityRemoteInterface) api.EVChargeStrategyType + + // returns the current energy demand + // + // parameters: + // - entity: the entity of the EV + // + // return values: + // - 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 + EnergyDemand(remoteEntity spineapi.EntityRemoteInterface) (api.Demand, error) + + // Scenario 2 + + TimeSlotConstraints(entity spineapi.EntityRemoteInterface) (api.TimeSlotConstraints, error) + + // send power limits to the EV + // + // parameters: + // - entity: the entity of the EV + // - data: the power limits + // + // if no data is provided, default power limits with the max possible value for 7 days will be sent + WritePowerLimits(entity spineapi.EntityRemoteInterface, data []api.DurationSlotValue) error + + // Scenario 3 + + // return the current incentive constraints + // + // parameters: + // - entity: the entity of the EV + IncentiveConstraints(entity spineapi.EntityRemoteInterface) (api.IncentiveSlotConstraints, error) + + // send new incentives to the EV + // + // parameters: + // - entity: the entity of the EV + // - data: the incentive descriptions + WriteIncentiveTableDescriptions(entity spineapi.EntityRemoteInterface, data []api.IncentiveTariffDescription) error + + // send incentives to the EV + // + // parameters: + // - entity: the entity of the EV + // - data: the incentives + // + // if no data is provided, default incentives with the same price for 7 days will be sent + WriteIncentives(entity spineapi.EntityRemoteInterface, data []api.DurationSlotValue) error + + // Scenario 4 + + // return the current charge plan constraints + // + // parameters: + // - entity: the entity of the EV + ChargePlanConstraints(entity spineapi.EntityRemoteInterface) ([]api.DurationSlotValue, error) + + // return the current charge plan of the EV + // + // parameters: + // - entity: the entity of the EV + ChargePlan(entity spineapi.EntityRemoteInterface) (api.ChargePlan, error) + + // Scenario 5 & 6 + + // this is automatically covered by the SPINE implementation +} diff --git a/uccevc/events.go b/uccevc/events.go new file mode 100644 index 0000000..1e36d6a --- /dev/null +++ b/uccevc/events.go @@ -0,0 +1,213 @@ +package uccevc + +import ( + "github.com/enbility/cemd/util" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// handle SPINE events +func (e *UCCEVC) HandleEvent(payload spineapi.EventPayload) { + // only about events from an EV entity or device changes for this remote device + + if !util.IsCompatibleEntity(payload.Entity, e.validEntityTypes) { + return + } + + if util.IsEntityConnected(payload) { + e.evConnected(payload.Entity) + return + } + + if payload.EventType != spineapi.EventTypeDataChange { + return + } + + if payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + + switch payload.Data.(type) { + case *model.TimeSeriesDescriptionListDataType: + e.evTimeSeriesDescriptionDataUpdate(payload) + + case *model.TimeSeriesListDataType: + e.evTimeSeriesDataUpdate(payload) + + case *model.IncentiveTableDescriptionDataType: + e.evIncentiveTableDescriptionDataUpdate(payload) + + case *model.IncentiveTableConstraintsDataType: + e.evIncentiveTableConstraintsDataUpdate(payload) + + case *model.IncentiveDataType: + e.evIncentiveTableDataUpdate(payload) + } +} + +// an EV was connected +func (e *UCCEVC) evConnected(entity spineapi.EntityRemoteInterface) { + // initialise features, e.g. subscriptions, descriptions + if evDeviceConfiguration, err := util.DeviceConfiguration(e.service, entity); err == nil { + if _, err := evDeviceConfiguration.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + // get device configuration descriptions + if _, err := evDeviceConfiguration.RequestDescriptions(); err != nil { + logging.Log().Debug(err) + } + } + + if evTimeSeries, err := util.TimeSeries(e.service, entity); err == nil { + if _, err := evTimeSeries.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + if _, err := evTimeSeries.Bind(); err != nil { + logging.Log().Debug(err) + } + + // get time series descriptions + if _, err := evTimeSeries.RequestDescriptions(); err != nil { + logging.Log().Debug(err) + } + + // get time series constraints + if _, err := evTimeSeries.RequestConstraints(); err != nil { + logging.Log().Debug(err) + } + } + + if evIncentiveTable, err := util.IncentiveTable(e.service, entity); err == nil { + if _, err := evIncentiveTable.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + if _, err := evIncentiveTable.Bind(); err != nil { + logging.Log().Debug(err) + } + + // get incentivetable descriptions + if _, err := evIncentiveTable.RequestDescriptions(); err != nil { + logging.Log().Debug(err) + } + } +} + +// the time series description data of an EV was updated +func (e *UCCEVC) evTimeSeriesDescriptionDataUpdate(payload spineapi.EventPayload) { + if evTimeSeries, err := util.TimeSeries(e.service, payload.Entity); err == nil { + // get time series values + if _, err := evTimeSeries.RequestValues(); err != nil { + logging.Log().Debug(err) + } + } + + // check if we are required to update the plan + if !e.evCheckTimeSeriesDescriptionConstraintsUpdateRequired(payload.Entity) { + return + } + + _, err := e.EnergyDemand(payload.Entity) + if err != nil { + return + } + + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateEnergyDemand) + + _, err = e.TimeSlotConstraints(payload.Entity) + if err != nil { + logging.Log().Error("Error getting timeseries constraints:", err) + return + } + + _, err = e.IncentiveConstraints(payload.Entity) + if err != nil { + logging.Log().Error("Error getting incentive constraints:", err) + return + } + + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataRequestedPowerLimitsAndIncentives) +} + +// the load control limit data of an EV was updated +func (e *UCCEVC) evTimeSeriesDataUpdate(payload spineapi.EventPayload) { + if _, err := e.ChargePlan(payload.Entity); err == nil { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateChargePlan) + } + + if _, err := e.ChargePlanConstraints(payload.Entity); err == nil { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateTimeSlotConstraints) + } +} + +// the incentive table description data of an EV was updated +func (e *UCCEVC) evIncentiveTableDescriptionDataUpdate(payload spineapi.EventPayload) { + if evIncentiveTable, err := util.IncentiveTable(e.service, payload.Entity); err == nil { + // get time series values + if _, err := evIncentiveTable.RequestValues(); err != nil { + logging.Log().Debug(err) + } + } + + // check if we are required to update the plan + if e.evCheckIncentiveTableDescriptionUpdateRequired(payload.Entity) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataRequestedIncentiveTableDescription) + } +} + +// the incentive table constraint data of an EV was updated +func (e *UCCEVC) evIncentiveTableConstraintsDataUpdate(payload spineapi.EventPayload) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateIncentiveTable) +} + +// the incentive table data of an EV was updated +func (e *UCCEVC) evIncentiveTableDataUpdate(payload spineapi.EventPayload) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateIncentiveTable) +} + +// check timeSeries descriptions if constraints element has updateRequired set to true +// as this triggers the CEM to send power tables within 20s +func (e *UCCEVC) evCheckTimeSeriesDescriptionConstraintsUpdateRequired(entity spineapi.EntityRemoteInterface) bool { + evTimeSeries, err := util.TimeSeries(e.service, entity) + if err != nil { + logging.Log().Error("timeseries feature not found") + return false + } + + data, err := 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 *UCCEVC) evCheckIncentiveTableDescriptionUpdateRequired(entity spineapi.EntityRemoteInterface) bool { + evIncentiveTable, err := util.IncentiveTable(e.service, entity) + if err != nil { + logging.Log().Error("incentivetable feature not found") + return false + } + + data, err := evIncentiveTable.GetDescriptionsForScope(model.ScopeTypeTypeSimpleIncentiveTable) + if err != nil || len(data) == 0 { + return false + } + + // only use the first description and therein the first tariff + item := data[0].TariffDescription + if item != nil && item.UpdateRequired != nil { + return *item.UpdateRequired + } + + return false +} diff --git a/uccevc/events_test.go b/uccevc/events_test.go new file mode 100644 index 0000000..7e858fe --- /dev/null +++ b/uccevc/events_test.go @@ -0,0 +1,161 @@ +package uccevc + +import ( + "time" + + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCCEVCSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.evEntity + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeEntityChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.Data = eebusutil.Ptr(model.TimeSeriesDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.TimeSeriesListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.IncentiveTableDescriptionDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.IncentiveTableConstraintsDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.IncentiveDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *UCCEVCSuite) Test_Failures() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.evConnected(s.mockRemoteEntity) + + s.sut.evTimeSeriesDescriptionDataUpdate(payload) + + s.sut.evTimeSeriesDataUpdate(payload) + + s.sut.evIncentiveTableDescriptionDataUpdate(payload) + + s.sut.evCheckTimeSeriesDescriptionConstraintsUpdateRequired(s.mockRemoteEntity) + + s.sut.evCheckIncentiveTableDescriptionUpdateRequired(s.mockRemoteEntity) +} + +func (s *UCCEVCSuite) Test_evTimeSeriesDescriptionDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evTimeSeriesDescriptionDataUpdate(payload) + + payload.Entity = s.evEntity + s.sut.evTimeSeriesDescriptionDataUpdate(payload) + + timeDesc := &model.TimeSeriesDescriptionListDataType{ + TimeSeriesDescriptionData: []model.TimeSeriesDescriptionDataType{ + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesType: eebusutil.Ptr(model.TimeSeriesTypeTypeConstraints), + UpdateRequired: eebusutil.Ptr(true), + }, + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(1)), + TimeSeriesType: eebusutil.Ptr(model.TimeSeriesTypeTypeSingleDemand), + }, + }, + } + + rTimeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeServer) + fErr := rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesDescriptionListData, timeDesc, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evTimeSeriesDescriptionDataUpdate(payload) + + timeData := &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(1)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(0)), + MinValue: model.NewScaledNumberType(1000), + Value: model.NewScaledNumberType(10000), + MaxValue: model.NewScaledNumberType(100000), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + demand, err := s.sut.EnergyDemand(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 1000.0, demand.MinDemand) + assert.Equal(s.T(), 10000.0, demand.OptDemand) + assert.Equal(s.T(), 100000.0, demand.MaxDemand) + assert.Equal(s.T(), 0.0, demand.DurationUntilStart) + assert.Equal(s.T(), 0.0, demand.DurationUntilEnd) + + s.sut.evTimeSeriesDescriptionDataUpdate(payload) + + constData := &model.TimeSeriesConstraintsListDataType{ + TimeSeriesConstraintsData: []model.TimeSeriesConstraintsDataType{ + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(0)), + SlotCountMin: eebusutil.Ptr(model.TimeSeriesSlotCountType(1)), + SlotCountMax: eebusutil.Ptr(model.TimeSeriesSlotCountType(10)), + SlotDurationMin: model.NewDurationType(1 * time.Minute), + SlotDurationMax: model.NewDurationType(60 * time.Minute), + SlotDurationStepSize: model.NewDurationType(1 * time.Minute), + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesConstraintsListData, constData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evTimeSeriesDescriptionDataUpdate(payload) + + incConstData := &model.IncentiveTableConstraintsDataType{ + IncentiveTableConstraints: []model.IncentiveTableConstraintsType{ + { + IncentiveSlotConstraints: &model.TimeTableConstraintsDataType{ + SlotCountMin: eebusutil.Ptr(model.TimeSlotCountType(1)), + SlotCountMax: eebusutil.Ptr(model.TimeSlotCountType(10)), + }, + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeIncentiveTable, model.RoleTypeServer) + fErr = rFeature.UpdateData(model.FunctionTypeIncentiveTableConstraintsData, incConstData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evTimeSeriesDescriptionDataUpdate(payload) +} diff --git a/uccevc/public_scen1.go b/uccevc/public_scen1.go new file mode 100644 index 0000000..fa7f23b --- /dev/null +++ b/uccevc/public_scen1.go @@ -0,0 +1,134 @@ +package uccevc + +import ( + "time" + + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// returns the current charging strategy +func (e *UCCEVC) ChargeStrategy(entity spineapi.EntityRemoteInterface) api.EVChargeStrategyType { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return api.EVChargeStrategyTypeUnknown + } + + evTimeSeries, err := util.TimeSeries(e.service, entity) + if err != nil { + return api.EVChargeStrategyTypeUnknown + } + + // only the time series data for singledemand is relevant for detecting the charging strategy + data, err := evTimeSeries.GetValueForType(model.TimeSeriesTypeTypeSingleDemand) + if err != nil { + return api.EVChargeStrategyTypeUnknown + } + + // without time series slots, there is no known strategy + if data.TimeSeriesSlot == nil || len(data.TimeSeriesSlot) == 0 { + return api.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 && firstSlot.Value.GetValue() > 0 { + return api.EVChargeStrategyTypeDirectCharging + } + + // maxValue will show the maximum amount the battery could take + return api.EVChargeStrategyTypeNoDemand + + case firstSlot.Duration != nil: + if _, err := firstSlot.Duration.GetTimeDuration(); err != nil { + // we got an invalid duration + return api.EVChargeStrategyTypeUnknown + } + + if firstSlot.MinValue != nil && firstSlot.MinValue.GetValue() > 0 { + return api.EVChargeStrategyTypeMinSoC + } + + if firstSlot.Value != nil { + if firstSlot.Value.GetValue() > 0 { + // there is demand and a duration + return api.EVChargeStrategyTypeTimedCharging + } + + return api.EVChargeStrategyTypeNoDemand + } + } + + return api.EVChargeStrategyTypeUnknown +} + +// returns the current energy demand in Wh and the duration +func (e *UCCEVC) EnergyDemand(entity spineapi.EntityRemoteInterface) (api.Demand, error) { + demand := api.Demand{} + + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return demand, api.ErrNoCompatibleEntity + } + + evTimeSeries, err := util.TimeSeries(e.service, entity) + if err != nil { + return demand, eebusapi.ErrDataNotAvailable + } + + data, err := evTimeSeries.GetValueForType(model.TimeSeriesTypeTypeSingleDemand) + if err != nil { + return demand, eebusapi.ErrDataNotAvailable + } + + // we need at least a time series slot + if data.TimeSeriesSlot == nil { + return demand, eebusapi.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.Seconds() + } + } + + // 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.Seconds() + + return demand, nil +} diff --git a/uccevc/public_scen1_test.go b/uccevc/public_scen1_test.go new file mode 100644 index 0000000..9fea86f --- /dev/null +++ b/uccevc/public_scen1_test.go @@ -0,0 +1,295 @@ +package uccevc + +import ( + "time" + + "github.com/enbility/cemd/api" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCCEVCSuite) Test_ChargeStrategy() { + data := s.sut.ChargeStrategy(s.mockRemoteEntity) + assert.Equal(s.T(), api.EVChargeStrategyTypeUnknown, data) + + data = s.sut.ChargeStrategy(s.evEntity) + assert.Equal(s.T(), api.EVChargeStrategyTypeUnknown, data) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeCommunicationsStandard), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data = s.sut.ChargeStrategy(s.evEntity) + assert.Equal(s.T(), api.EVChargeStrategyTypeUnknown, data) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + String: eebusutil.Ptr(model.DeviceConfigurationKeyValueStringType(model.DeviceConfigurationKeyValueStringTypeISO151182ED2)), + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + data = s.sut.ChargeStrategy(s.evEntity) + assert.Equal(s.T(), api.EVChargeStrategyTypeUnknown, data) + + timeDescData := &model.TimeSeriesDescriptionListDataType{ + TimeSeriesDescriptionData: []model.TimeSeriesDescriptionDataType{ + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesType: eebusutil.Ptr(model.TimeSeriesTypeTypeSingleDemand), + }, + }, + } + + rTimeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeServer) + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesDescriptionListData, timeDescData, nil, nil) + assert.Nil(s.T(), fErr) + + timeData := &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(0)), + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + data = s.sut.ChargeStrategy(s.evEntity) + assert.Equal(s.T(), api.EVChargeStrategyTypeUnknown, data) + + timeData = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(0)), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + data = s.sut.ChargeStrategy(s.evEntity) + assert.Equal(s.T(), api.EVChargeStrategyTypeNoDemand, data) + + timeData = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(0)), + Duration: eebusutil.Ptr(model.DurationType("PT0S")), + Value: model.NewScaledNumberType(0), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + data = s.sut.ChargeStrategy(s.evEntity) + assert.Equal(s.T(), api.EVChargeStrategyTypeNoDemand, data) + + timeData = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(0)), + Value: model.NewScaledNumberType(10000), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + data = s.sut.ChargeStrategy(s.evEntity) + assert.Equal(s.T(), api.EVChargeStrategyTypeDirectCharging, data) + + timeData = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(0)), + Value: model.NewScaledNumberType(10000), + Duration: model.NewDurationType(2 * time.Hour), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + data = s.sut.ChargeStrategy(s.evEntity) + assert.Equal(s.T(), api.EVChargeStrategyTypeTimedCharging, data) +} + +func (s *UCCEVCSuite) Test_EnergySingleDemand() { + demand, err := s.sut.EnergyDemand(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, demand.MinDemand) + assert.Equal(s.T(), 0.0, demand.OptDemand) + assert.Equal(s.T(), 0.0, demand.MaxDemand) + assert.Equal(s.T(), 0.0, demand.DurationUntilStart) + assert.Equal(s.T(), 0.0, demand.DurationUntilEnd) + + demand, err = s.sut.EnergyDemand(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, demand.MinDemand) + assert.Equal(s.T(), 0.0, demand.OptDemand) + assert.Equal(s.T(), 0.0, demand.MaxDemand) + assert.Equal(s.T(), 0.0, demand.DurationUntilStart) + assert.Equal(s.T(), 0.0, demand.DurationUntilEnd) + + timeDescData := &model.TimeSeriesDescriptionListDataType{ + TimeSeriesDescriptionData: []model.TimeSeriesDescriptionDataType{ + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesType: eebusutil.Ptr(model.TimeSeriesTypeTypeSingleDemand), + }, + }, + } + + rTimeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeServer) + fErr := rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesDescriptionListData, timeDescData, nil, nil) + assert.Nil(s.T(), fErr) + + timeData := &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(0)), + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + demand, err = s.sut.EnergyDemand(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, demand.MinDemand) + assert.Equal(s.T(), 0.0, demand.OptDemand) + assert.Equal(s.T(), 0.0, demand.MaxDemand) + assert.Equal(s.T(), 0.0, demand.DurationUntilStart) + assert.Equal(s.T(), 0.0, demand.DurationUntilEnd) + + timeData = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(0)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + demand, err = s.sut.EnergyDemand(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 0.0, demand.MinDemand) + assert.Equal(s.T(), 0.0, demand.OptDemand) + assert.Equal(s.T(), 0.0, demand.MaxDemand) + assert.Equal(s.T(), 0.0, demand.DurationUntilStart) + assert.Equal(s.T(), 0.0, demand.DurationUntilEnd) + + timeData = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(0)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(0)), + MinValue: model.NewScaledNumberType(1000), + Value: model.NewScaledNumberType(10000), + MaxValue: model.NewScaledNumberType(100000), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + demand, err = s.sut.EnergyDemand(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 1000.0, demand.MinDemand) + assert.Equal(s.T(), 10000.0, demand.OptDemand) + assert.Equal(s.T(), 100000.0, demand.MaxDemand) + assert.Equal(s.T(), 0.0, demand.DurationUntilStart) + assert.Equal(s.T(), 0.0, demand.DurationUntilEnd) + + timeData = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(0)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(0)), + Value: model.NewScaledNumberType(10000), + Duration: model.NewDurationType(2 * time.Hour), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + demand, err = s.sut.EnergyDemand(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 0.0, demand.MinDemand) + assert.Equal(s.T(), 10000.0, demand.OptDemand) + assert.Equal(s.T(), 0.0, demand.MaxDemand) + assert.Equal(s.T(), 0.0, demand.DurationUntilStart) + assert.Equal(s.T(), time.Duration(2*time.Hour).Seconds(), demand.DurationUntilEnd) +} diff --git a/uccevc/public_scen2.go b/uccevc/public_scen2.go new file mode 100644 index 0000000..9b65157 --- /dev/null +++ b/uccevc/public_scen2.go @@ -0,0 +1,172 @@ +package uccevc + +import ( + "errors" + "time" + + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// returns the constraints for the time slots +func (e *UCCEVC) TimeSlotConstraints(entity spineapi.EntityRemoteInterface) (api.TimeSlotConstraints, error) { + result := api.TimeSlotConstraints{} + + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return result, api.ErrNoCompatibleEntity + } + + evTimeSeries, err := util.TimeSeries(e.service, entity) + if err != nil { + return result, eebusapi.ErrDataNotAvailable + } + + constraints, err := evTimeSeries.GetConstraints() + if err != nil { + return result, err + } + + // 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, nil +} + +// send power limits to the EV +// if no data is provided, default power limits with the max possible value for 7 days will be sent +func (e *UCCEVC) WritePowerLimits(entity spineapi.EntityRemoteInterface, data []api.DurationSlotValue) error { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return api.ErrNoCompatibleEntity + } + + evTimeSeries, err := util.TimeSeries(e.service, entity) + if err != nil { + return eebusapi.ErrDataNotAvailable + } + + if len(data) == 0 { + data, err = e.defaultPowerLimits(entity) + if err != nil { + return err + } + } + + constraints, err := e.TimeSlotConstraints(entity) + if err != nil { + return err + } + + 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 := evTimeSeries.GetDescriptionForType(model.TimeSeriesTypeTypeConstraints) + if err != nil { + return eebusapi.ErrDataNotAvailable + } + + 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 = evTimeSeries.WriteValues([]model.TimeSeriesDataType{timeSeriesData}) + + return err +} + +func (e *UCCEVC) defaultPowerLimits(entity spineapi.EntityRemoteInterface) ([]api.DurationSlotValue, error) { + // send default power limits for the maximum timeframe + // to fullfill spec, as there is no data provided + logging.Log().Info("Fallback sending default power limits") + + evElectricalConnection, err := util.ElectricalConnection(e.service, entity) + if err != nil { + logging.Log().Error("electrical connection feature not found") + return nil, err + } + + paramDesc, err := evElectricalConnection.GetParameterDescriptionForScopeType(model.ScopeTypeTypeACPower) + if err != nil { + logging.Log().Error("Error getting parameter descriptions:", err) + return nil, err + } + + permitted, err := evElectricalConnection.GetPermittedValueSetForParameterId(*paramDesc.ParameterId) + if err != nil { + logging.Log().Error("Error getting permitted values:", err) + return nil, err + } + + if len(permitted.PermittedValueSet) < 1 || len(permitted.PermittedValueSet[0].Range) < 1 { + text := "No permitted value set available" + logging.Log().Error(text) + return nil, errors.New(text) + } + + data := []api.DurationSlotValue{ + { + Duration: 7 * time.Hour * 24, + Value: permitted.PermittedValueSet[0].Range[0].Max.GetValue(), + }, + } + return data, nil +} diff --git a/uccevc/public_scen2_test.go b/uccevc/public_scen2_test.go new file mode 100644 index 0000000..dbe3d98 --- /dev/null +++ b/uccevc/public_scen2_test.go @@ -0,0 +1,221 @@ +package uccevc + +import ( + "testing" + "time" + + "github.com/enbility/cemd/api" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/ship-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCCEVCSuite) Test_TimeSlotConstraints() { + constraints, err := s.sut.TimeSlotConstraints(s.mockRemoteEntity) + assert.Equal(s.T(), uint(0), constraints.MinSlots) + assert.Equal(s.T(), uint(0), constraints.MaxSlots) + assert.Equal(s.T(), time.Duration(0), constraints.MinSlotDuration) + assert.Equal(s.T(), time.Duration(0), constraints.MaxSlotDuration) + assert.Equal(s.T(), time.Duration(0), constraints.SlotDurationStepSize) + assert.NotEqual(s.T(), err, nil) + + constraints, err = s.sut.TimeSlotConstraints(s.evEntity) + assert.Equal(s.T(), uint(0), constraints.MinSlots) + assert.Equal(s.T(), uint(0), constraints.MaxSlots) + assert.Equal(s.T(), time.Duration(0), constraints.MinSlotDuration) + assert.Equal(s.T(), time.Duration(0), constraints.MaxSlotDuration) + assert.Equal(s.T(), time.Duration(0), constraints.SlotDurationStepSize) + assert.NotEqual(s.T(), err, nil) + + constData := &model.TimeSeriesConstraintsListDataType{ + TimeSeriesConstraintsData: []model.TimeSeriesConstraintsDataType{ + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(0)), + SlotCountMin: eebusutil.Ptr(model.TimeSeriesSlotCountType(1)), + SlotCountMax: eebusutil.Ptr(model.TimeSeriesSlotCountType(10)), + SlotDurationMin: model.NewDurationType(1 * time.Minute), + SlotDurationMax: model.NewDurationType(60 * time.Minute), + SlotDurationStepSize: model.NewDurationType(1 * time.Minute), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeTimeSeriesConstraintsListData, constData, nil, nil) + assert.Nil(s.T(), fErr) + + constraints, err = s.sut.TimeSlotConstraints(s.evEntity) + assert.Equal(s.T(), uint(1), constraints.MinSlots) + assert.Equal(s.T(), uint(10), constraints.MaxSlots) + assert.Equal(s.T(), time.Duration(1*time.Minute), constraints.MinSlotDuration) + assert.Equal(s.T(), time.Duration(1*time.Hour), constraints.MaxSlotDuration) + assert.Equal(s.T(), time.Duration(1*time.Minute), constraints.SlotDurationStepSize) + assert.Equal(s.T(), err, nil) +} + +func (s *UCCEVCSuite) Test_WritePowerLimits() { + data := []api.DurationSlotValue{} + + err := s.sut.WritePowerLimits(s.mockRemoteEntity, data) + assert.NotNil(s.T(), err) + + err = s.sut.WritePowerLimits(s.evEntity, data) + assert.NotNil(s.T(), err) + + elParamDesc := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeACPower), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, elParamDesc, nil, nil) + assert.Nil(s.T(), fErr) + + err = s.sut.WritePowerLimits(s.evEntity, data) + assert.NotNil(s.T(), err) + + elPermDesc := &model.ElectricalConnectionPermittedValueSetListDataType{ + ElectricalConnectionPermittedValueSetData: []model.ElectricalConnectionPermittedValueSetDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeElectricalConnectionPermittedValueSetListData, elPermDesc, nil, nil) + assert.Nil(s.T(), fErr) + + err = s.sut.WritePowerLimits(s.evEntity, data) + assert.NotNil(s.T(), err) + + elPermDesc = &model.ElectricalConnectionPermittedValueSetListDataType{ + ElectricalConnectionPermittedValueSetData: []model.ElectricalConnectionPermittedValueSetDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + PermittedValueSet: []model.ScaledNumberSetType{ + { + Range: []model.ScaledNumberRangeType{ + { + Max: model.NewScaledNumberType(16), + }, + }, + }, + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeElectricalConnectionPermittedValueSetListData, elPermDesc, nil, nil) + assert.Nil(s.T(), fErr) + + err = s.sut.WritePowerLimits(s.evEntity, data) + assert.NotNil(s.T(), err) + + descData := &model.TimeSeriesDescriptionListDataType{ + TimeSeriesDescriptionData: []model.TimeSeriesDescriptionDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeConstraints), + }, + }, + } + + rFeature = s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeServer) + fErr = rFeature.UpdateData(model.FunctionTypeTimeSeriesDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + err = s.sut.WritePowerLimits(s.evEntity, data) + assert.NotNil(s.T(), err) + + type dataStruct struct { + error bool + minSlots, maxSlots uint + slots []api.DurationSlotValue + } + + tests := []struct { + name string + data []dataStruct + }{ + { + "too few slots", + []dataStruct{ + { + true, 2, 2, + []api.DurationSlotValue{ + {Duration: time.Hour, Value: 11000}, + }, + }, + }, + }, { + "too many slots", + []dataStruct{ + { + true, 1, 1, + []api.DurationSlotValue{ + {Duration: time.Hour, Value: 11000}, + {Duration: time.Hour, Value: 11000}, + }, + }, + }, + }, + { + "1 slot", + []dataStruct{ + { + false, 1, 1, + []api.DurationSlotValue{ + {Duration: time.Hour, Value: 11000}, + }, + }, + }, + }, + { + "2 slots", + []dataStruct{ + { + false, 1, 2, + []api.DurationSlotValue{ + {Duration: time.Hour, Value: 11000}, + {Duration: 30 * time.Minute, Value: 5000}, + }, + }, + }, + }, + } + + for _, tc := range tests { + s.T().Run(tc.name, func(t *testing.T) { + for _, data := range tc.data { + constData := &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)), + }, + }, + } + + fErr := rFeature.UpdateData(model.FunctionTypeTimeSeriesConstraintsListData, constData, nil, nil) + assert.Nil(s.T(), fErr) + + err = s.sut.WritePowerLimits(s.evEntity, data.slots) + if data.error { + assert.NotNil(t, err) + continue + } + + assert.Nil(t, err) + } + }) + } +} diff --git a/uccevc/public_scen3.go b/uccevc/public_scen3.go new file mode 100644 index 0000000..9e90da9 --- /dev/null +++ b/uccevc/public_scen3.go @@ -0,0 +1,276 @@ +package uccevc + +import ( + "errors" + "time" + + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// returns the minimum and maximum number of incentive slots allowed +func (e *UCCEVC) IncentiveConstraints(entity spineapi.EntityRemoteInterface) (api.IncentiveSlotConstraints, error) { + result := api.IncentiveSlotConstraints{} + + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return result, api.ErrNoCompatibleEntity + } + + evIncentiveTable, err := util.IncentiveTable(e.service, entity) + if err != nil { + return result, eebusapi.ErrDataNotAvailable + } + + constraints, err := evIncentiveTable.GetConstraints() + if err != nil { + return result, err + } + + // 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, nil +} + +// inform the EVSE about used currency and boundary units +// +// SPINE UC CoordinatedEVCharging 2.4.3 +func (e *UCCEVC) WriteIncentiveTableDescriptions(entity spineapi.EntityRemoteInterface, data []api.IncentiveTariffDescription) error { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return api.ErrNoCompatibleEntity + } + + evIncentiveTable, err := util.IncentiveTable(e.service, entity) + if err != nil { + logging.Log().Error("incentivetable feature not found") + return err + } + + descriptions, err := evIncentiveTable.GetDescriptionsForScope(model.ScopeTypeTypeSimpleIncentiveTable) + if err != nil { + logging.Log().Error(err) + return err + } + + // default tariff + // + // - 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! + descData := []model.IncentiveTableDescriptionType{ + { + TariffDescription: descriptions[0].TariffDescription, + Tier: []model.IncentiveTableDescriptionTierType{ + { + TierDescription: &model.TierDescriptionDataType{ + TierId: eebusutil.Ptr(model.TierIdType(0)), + TierType: eebusutil.Ptr(model.TierTypeTypeDynamicCost), + }, + BoundaryDescription: []model.TierBoundaryDescriptionDataType{ + { + BoundaryId: eebusutil.Ptr(model.TierBoundaryIdType(0)), + BoundaryType: eebusutil.Ptr(model.TierBoundaryTypeTypePowerBoundary), + BoundaryUnit: eebusutil.Ptr(model.UnitOfMeasurementTypeW), + }, + }, + IncentiveDescription: []model.IncentiveDescriptionDataType{ + { + IncentiveId: eebusutil.Ptr(model.IncentiveIdType(0)), + IncentiveType: eebusutil.Ptr(model.IncentiveTypeTypeAbsoluteCost), + Currency: eebusutil.Ptr(model.CurrencyTypeEur), + }, + }, + }, + }, + }, + } + + if len(data) > 0 && len(data[0].Tiers) > 0 { + newDescData := []model.IncentiveTableDescriptionType{} + allDataPresent := false + + for index, tariff := range data { + tariffDesc := descriptions[0].TariffDescription + if len(descriptions) > index { + tariffDesc = descriptions[index].TariffDescription + } + + newTariff := model.IncentiveTableDescriptionType{ + TariffDescription: tariffDesc, + } + + tierData := []model.IncentiveTableDescriptionTierType{} + for _, tier := range tariff.Tiers { + newTier := model.IncentiveTableDescriptionTierType{} + + newTier.TierDescription = &model.TierDescriptionDataType{ + TierId: eebusutil.Ptr(model.TierIdType(tier.Id)), + TierType: eebusutil.Ptr(tier.Type), + } + + boundaryDescription := []model.TierBoundaryDescriptionDataType{} + for _, boundary := range tier.Boundaries { + newBoundary := model.TierBoundaryDescriptionDataType{ + BoundaryId: eebusutil.Ptr(model.TierBoundaryIdType(boundary.Id)), + BoundaryType: eebusutil.Ptr(boundary.Type), + BoundaryUnit: eebusutil.Ptr(boundary.Unit), + } + boundaryDescription = append(boundaryDescription, newBoundary) + } + newTier.BoundaryDescription = boundaryDescription + + incentiveDescription := []model.IncentiveDescriptionDataType{} + for _, incentive := range tier.Incentives { + newIncentive := model.IncentiveDescriptionDataType{ + IncentiveId: eebusutil.Ptr(model.IncentiveIdType(incentive.Id)), + IncentiveType: eebusutil.Ptr(incentive.Type), + } + if incentive.Currency != "" { + newIncentive.Currency = eebusutil.Ptr(incentive.Currency) + } + incentiveDescription = append(incentiveDescription, newIncentive) + } + newTier.IncentiveDescription = incentiveDescription + + if len(newTier.BoundaryDescription) > 0 && + len(newTier.IncentiveDescription) > 0 { + allDataPresent = true + } + tierData = append(tierData, newTier) + } + + newTariff.Tier = tierData + + newDescData = append(newDescData, newTariff) + } + + if allDataPresent { + descData = newDescData + } + } + + _, err = evIncentiveTable.WriteDescriptions(descData) + if err != nil { + logging.Log().Error(err) + return err + } + + return nil +} + +// send incentives to the EV +// if no data is provided, default incentives with the same price for 7 days will be sent +func (e *UCCEVC) WriteIncentives(entity spineapi.EntityRemoteInterface, data []api.DurationSlotValue) error { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return api.ErrNoCompatibleEntity + } + + evIncentiveTable, err := util.IncentiveTable(e.service, entity) + if err != nil { + return eebusapi.ErrDataNotAvailable + } + + if len(data) == 0 { + // send default incentives for the maximum timeframe + // to fullfill spec, as there is no data provided + logging.Log().Info("Fallback sending default incentives") + data = []api.DurationSlotValue{ + {Duration: 7 * time.Hour * 24, Value: 0.30}, + } + } + + constraints, err := e.IncentiveConstraints(entity) + if err != nil { + return err + } + + 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(0)), + }, + Boundary: []model.TierBoundaryDataType{ + { + BoundaryId: eebusutil.Ptr(model.TierBoundaryIdType(0)), // only 1 boundary exists + LowerBoundaryValue: model.NewScaledNumberType(0), + }, + }, + Incentive: []model.IncentiveDataType{ + { + IncentiveId: eebusutil.Ptr(model.IncentiveIdType(0)), // 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 = evIncentiveTable.WriteValues([]model.IncentiveTableType{incentiveData}) + + return err +} diff --git a/uccevc/public_scen3_test.go b/uccevc/public_scen3_test.go new file mode 100644 index 0000000..85dc458 --- /dev/null +++ b/uccevc/public_scen3_test.go @@ -0,0 +1,230 @@ +package uccevc + +import ( + "testing" + "time" + + "github.com/enbility/cemd/api" + "github.com/enbility/ship-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCCEVCSuite) Test_IncentiveConstraints() { + constraints, err := s.sut.IncentiveConstraints(s.mockRemoteEntity) + assert.Equal(s.T(), uint(0), constraints.MinSlots) + assert.Equal(s.T(), uint(0), constraints.MaxSlots) + assert.NotEqual(s.T(), err, nil) + + constraints, err = s.sut.IncentiveConstraints(s.evEntity) + assert.Equal(s.T(), uint(0), constraints.MinSlots) + assert.Equal(s.T(), uint(0), constraints.MaxSlots) + assert.NotEqual(s.T(), err, nil) + + constData := &model.IncentiveTableConstraintsDataType{ + IncentiveTableConstraints: []model.IncentiveTableConstraintsType{ + { + IncentiveSlotConstraints: &model.TimeTableConstraintsDataType{ + SlotCountMin: util.Ptr(model.TimeSlotCountType(1)), + SlotCountMax: util.Ptr(model.TimeSlotCountType(10)), + }, + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeIncentiveTable, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeIncentiveTableConstraintsData, constData, nil, nil) + assert.Nil(s.T(), fErr) + + constraints, err = s.sut.IncentiveConstraints(s.evEntity) + assert.Equal(s.T(), uint(1), constraints.MinSlots) + assert.Equal(s.T(), uint(10), constraints.MaxSlots) + assert.Equal(s.T(), err, nil) + + constData = &model.IncentiveTableConstraintsDataType{ + IncentiveTableConstraints: []model.IncentiveTableConstraintsType{ + { + IncentiveSlotConstraints: &model.TimeTableConstraintsDataType{ + SlotCountMin: util.Ptr(model.TimeSlotCountType(1)), + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeIncentiveTableConstraintsData, constData, nil, nil) + assert.Nil(s.T(), fErr) + + constraints, err = s.sut.IncentiveConstraints(s.evEntity) + assert.Equal(s.T(), uint(1), constraints.MinSlots) + assert.Equal(s.T(), uint(0), constraints.MaxSlots) + assert.Equal(s.T(), err, nil) +} + +func (s *UCCEVCSuite) Test_WriteIncentiveTableDescriptions() { + data := []api.IncentiveTariffDescription{} + + err := s.sut.WriteIncentiveTableDescriptions(s.mockRemoteEntity, data) + assert.NotNil(s.T(), err) + + err = s.sut.WriteIncentiveTableDescriptions(s.evEntity, data) + assert.NotNil(s.T(), err) + + descData := &model.IncentiveTableDescriptionDataType{ + IncentiveTableDescription: []model.IncentiveTableDescriptionType{ + { + TariffDescription: &model.TariffDescriptionDataType{ + TariffId: util.Ptr(model.TariffIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeSimpleIncentiveTable), + }, + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeIncentiveTable, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeIncentiveTableDescriptionData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + err = s.sut.WriteIncentiveTableDescriptions(s.evEntity, data) + assert.Nil(s.T(), err) + + data = []api.IncentiveTariffDescription{ + { + Tiers: []api.IncentiveTableDescriptionTier{ + { + Id: 0, + Type: model.TierTypeTypeDynamicCost, + Boundaries: []api.TierBoundaryDescription{ + { + Id: 0, + Type: model.TierBoundaryTypeTypePowerBoundary, + Unit: model.UnitOfMeasurementTypeW, + }, + }, + Incentives: []api.IncentiveDescription{ + { + Id: 0, + Type: model.IncentiveTypeTypeAbsoluteCost, + Currency: model.CurrencyTypeEur, + }, + }, + }, + }, + }, + } + + err = s.sut.WriteIncentiveTableDescriptions(s.evEntity, data) + assert.Nil(s.T(), err) +} + +func (s *UCCEVCSuite) Test_WriteIncentives() { + data := []api.DurationSlotValue{} + + err := s.sut.WriteIncentives(s.mockRemoteEntity, data) + assert.NotNil(s.T(), err) + + err = s.sut.WriteIncentives(s.evEntity, data) + assert.NotNil(s.T(), err) + + constData := &model.IncentiveTableConstraintsDataType{ + IncentiveTableConstraints: []model.IncentiveTableConstraintsType{ + { + IncentiveSlotConstraints: &model.TimeTableConstraintsDataType{ + SlotCountMin: util.Ptr(model.TimeSlotCountType(1)), + SlotCountMax: util.Ptr(model.TimeSlotCountType(10)), + }, + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeIncentiveTable, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeIncentiveTableConstraintsData, constData, nil, nil) + assert.Nil(s.T(), fErr) + + err = s.sut.WriteIncentives(s.evEntity, data) + assert.Nil(s.T(), err) + + type dataStruct struct { + error bool + minSlots, maxSlots uint + slots []api.DurationSlotValue + } + + tests := []struct { + name string + data []dataStruct + }{ + { + "too few slots", + []dataStruct{ + { + true, 2, 2, + []api.DurationSlotValue{ + {Duration: time.Hour, Value: 0.1}, + }, + }, + }, + }, { + "too many slots", + []dataStruct{ + { + true, 1, 1, + []api.DurationSlotValue{ + {Duration: time.Hour, Value: 0.1}, + {Duration: time.Hour, Value: 0.1}, + }, + }, + }, + }, + { + "1 slot", + []dataStruct{ + { + false, 1, 1, + []api.DurationSlotValue{ + {Duration: time.Hour, Value: 0.1}, + }, + }, + }, + }, + { + "2 slots", + []dataStruct{ + { + false, 1, 2, + []api.DurationSlotValue{ + {Duration: time.Hour, Value: 0.1}, + {Duration: 30 * time.Minute, Value: 0.2}, + }, + }, + }, + }, + } + + for _, tc := range tests { + s.T().Run(tc.name, func(t *testing.T) { + for _, data := range tc.data { + constData = &model.IncentiveTableConstraintsDataType{ + IncentiveTableConstraints: []model.IncentiveTableConstraintsType{ + { + IncentiveSlotConstraints: &model.TimeTableConstraintsDataType{ + SlotCountMin: util.Ptr(model.TimeSlotCountType(data.minSlots)), + SlotCountMax: util.Ptr(model.TimeSlotCountType(data.maxSlots)), + }, + }, + }, + } + + fErr := rFeature.UpdateData(model.FunctionTypeIncentiveTableConstraintsData, constData, nil, nil) + assert.Nil(s.T(), fErr) + + err = s.sut.WriteIncentives(s.evEntity, data.slots) + if data.error { + assert.NotNil(t, err) + continue + } + + assert.Nil(t, err) + } + }) + } +} diff --git a/uccevc/public_scen4.go b/uccevc/public_scen4.go new file mode 100644 index 0000000..9a99a35 --- /dev/null +++ b/uccevc/public_scen4.go @@ -0,0 +1,146 @@ +package uccevc + +import ( + "time" + + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +func (e *UCCEVC) ChargePlanConstraints(entity spineapi.EntityRemoteInterface) ([]api.DurationSlotValue, error) { + constraints := []api.DurationSlotValue{} + + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return constraints, api.ErrNoCompatibleEntity + } + + evTimeSeries, err := util.TimeSeries(e.service, entity) + if err != nil { + return constraints, eebusapi.ErrDataNotAvailable + } + + data, err := evTimeSeries.GetValueForType(model.TimeSeriesTypeTypeConstraints) + if err != nil { + return constraints, eebusapi.ErrDataNotAvailable + } + + // we need at least a time series slot + if data.TimeSeriesSlot == nil { + return constraints, eebusapi.ErrDataNotAvailable + } + + // get the values for all slots + for _, slot := range data.TimeSeriesSlot { + newSlot := api.DurationSlotValue{} + + if slot.Duration != nil { + if duration, err := slot.Duration.GetTimeDuration(); err == nil { + newSlot.Duration = duration + } + } else if slot.TimePeriod != nil { + var slotStart, slotEnd time.Time + if slot.TimePeriod.StartTime != nil { + if time, err := slot.TimePeriod.StartTime.GetTime(); err == nil { + slotStart = time + } + } + if slot.TimePeriod.EndTime != nil { + if time, err := slot.TimePeriod.EndTime.GetTime(); err == nil { + slotEnd = time + } + } + newSlot.Duration = slotEnd.Sub(slotStart) + } + + if slot.MaxValue != nil { + newSlot.Value = slot.MaxValue.GetValue() + } + + constraints = append(constraints, newSlot) + } + + return constraints, nil +} + +func (e *UCCEVC) ChargePlan(entity spineapi.EntityRemoteInterface) (api.ChargePlan, error) { + plan := api.ChargePlan{} + + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return plan, api.ErrNoCompatibleEntity + } + + evTimeSeries, err := util.TimeSeries(e.service, entity) + if err != nil { + return plan, eebusapi.ErrDataNotAvailable + } + + data, err := evTimeSeries.GetValueForType(model.TimeSeriesTypeTypePlan) + if err != nil { + return plan, eebusapi.ErrDataNotAvailable + } + + // we need at least a time series slot + if data.TimeSeriesSlot == nil { + return plan, eebusapi.ErrDataNotAvailable + } + + startAvailable := false + // check the start time relative to now of the plan, default is now + currentStart := time.Now() + currentEnd := currentStart + if data.TimePeriod != nil && data.TimePeriod.StartTime != nil { + if start, err := data.TimePeriod.StartTime.GetTimeDuration(); err == nil { + currentStart = currentStart.Add(start) + startAvailable = true + } + } + + // get the values for all slots + for index, slot := range data.TimeSeriesSlot { + newSlot := api.ChargePlanSlotValue{} + + slotStartDefined := false + if index == 0 && startAvailable && (slot.TimePeriod == nil || slot.TimePeriod.StartTime == nil) { + newSlot.Start = currentStart + slotStartDefined = true + } + if slot.TimePeriod != nil && slot.TimePeriod.StartTime != nil { + if time, err := slot.TimePeriod.StartTime.GetTime(); err == nil { + newSlot.Start = time + slotStartDefined = true + } + } + if !slotStartDefined { + newSlot.Start = currentEnd + } + + if slot.Duration != nil { + if duration, err := slot.Duration.GetTimeDuration(); err == nil { + newSlot.End = newSlot.Start.Add(duration) + currentEnd = newSlot.End + } + } else if slot.TimePeriod != nil && slot.TimePeriod.EndTime != nil { + if time, err := slot.TimePeriod.StartTime.GetTime(); err == nil { + newSlot.End = time + currentEnd = newSlot.End + } + } + + if slot.Value != nil { + newSlot.Value = slot.Value.GetValue() + } + if slot.MinValue != nil { + newSlot.MinValue = slot.MinValue.GetValue() + } + if slot.MaxValue != nil { + newSlot.MaxValue = slot.MaxValue.GetValue() + } + + plan.Slots = append(plan.Slots, newSlot) + } + + return plan, nil +} diff --git a/uccevc/public_scen4_test.go b/uccevc/public_scen4_test.go new file mode 100644 index 0000000..9265e4f --- /dev/null +++ b/uccevc/public_scen4_test.go @@ -0,0 +1,175 @@ +package uccevc + +import ( + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/ship-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCCEVCSuite) Test_ChargePlanConstaints() { + _, err := s.sut.ChargePlanConstraints(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.ChargePlanConstraints(s.evEntity) + assert.NotNil(s.T(), err) + + descData := &model.TimeSeriesDescriptionListDataType{ + TimeSeriesDescriptionData: []model.TimeSeriesDescriptionDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(1)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeConstraints), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeTimeSeriesDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.ChargePlanConstraints(s.evEntity) + assert.NotNil(s.T(), err) + + data := &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(1)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeTimeSeriesListData, data, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.ChargePlanConstraints(s.evEntity) + assert.NotNil(s.T(), err) + + data = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(1)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(0)), + Duration: eebusutil.Ptr(model.DurationType("PT5M36S")), + MaxValue: model.NewScaledNumberType(4201), + }, + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(1)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT30S"), + EndTime: model.NewAbsoluteOrRelativeTimeType("PT1M"), + }, + MaxValue: model.NewScaledNumberType(4201), + }, + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeTimeSeriesListData, data, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.ChargePlanConstraints(s.evEntity) + assert.Nil(s.T(), err) +} + +func (s *UCCEVCSuite) Test_ChargePlan() { + _, err := s.sut.ChargePlan(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.ChargePlan(s.evEntity) + assert.NotNil(s.T(), err) + + descData := &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), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeTimeSeriesDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.ChargePlan(s.evEntity) + assert.NotNil(s.T(), err) + + timeData := &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(1)), + TimePeriod: &model.TimePeriodType{}, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(0)), + Duration: eebusutil.Ptr(model.DurationType("PT5M36S")), + MaxValue: model.NewScaledNumberType(4201), + }, + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(1)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT30S"), + EndTime: model.NewAbsoluteOrRelativeTimeType("PT1M"), + }, + Value: model.NewScaledNumberType(5), + MinValue: model.NewScaledNumberType(0), + MaxValue: model.NewScaledNumberType(10), + }, + }, + }, + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(2)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(0)), + Duration: eebusutil.Ptr(model.DurationType("PT5M36S")), + MaxValue: model.NewScaledNumberType(4201), + }, + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(1)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT30S"), + EndTime: model.NewAbsoluteOrRelativeTimeType("PT1M"), + }, + Value: model.NewScaledNumberType(5), + MinValue: model.NewScaledNumberType(0), + MaxValue: model.NewScaledNumberType(10), + }, + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.ChargePlan(s.evEntity) + assert.Nil(s.T(), err) +} diff --git a/uccevc/public_test.go b/uccevc/public_test.go new file mode 100644 index 0000000..83ab4f3 --- /dev/null +++ b/uccevc/public_test.go @@ -0,0 +1,339 @@ +package uccevc + +import ( + "time" + + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCCEVCSuite) Test_CoordinatedChargingScenarios() { + timeConst := &model.TimeSeriesConstraintsListDataType{ + TimeSeriesConstraintsData: []model.TimeSeriesConstraintsDataType{ + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(1)), + SlotCountMax: eebusutil.Ptr(model.TimeSeriesSlotCountType(30)), + }, + }, + } + + rTimeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeServer) + fErr := rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesConstraintsListData, timeConst, nil, nil) + assert.Nil(s.T(), fErr) + + timeDesc := &model.TimeSeriesDescriptionListDataType{ + TimeSeriesDescriptionData: []model.TimeSeriesDescriptionDataType{ + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(1)), + TimeSeriesType: eebusutil.Ptr(model.TimeSeriesTypeTypeConstraints), + TimeSeriesWriteable: eebusutil.Ptr(true), + UpdateRequired: eebusutil.Ptr(false), + Unit: eebusutil.Ptr(model.UnitOfMeasurementTypeW), + }, + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(2)), + TimeSeriesType: eebusutil.Ptr(model.TimeSeriesTypeTypePlan), + TimeSeriesWriteable: eebusutil.Ptr(false), + Unit: eebusutil.Ptr(model.UnitOfMeasurementTypeW), + }, + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(3)), + TimeSeriesType: eebusutil.Ptr(model.TimeSeriesTypeTypeSingleDemand), + TimeSeriesWriteable: eebusutil.Ptr(false), + Unit: eebusutil.Ptr(model.UnitOfMeasurementTypeWh), + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesDescriptionListData, timeDesc, nil, nil) + assert.Nil(s.T(), fErr) + + incDesc := &model.IncentiveTableDescriptionDataType{ + IncentiveTableDescription: []model.IncentiveTableDescriptionType{ + { + TariffDescription: &model.TariffDescriptionDataType{ + TariffId: eebusutil.Ptr(model.TariffIdType(1)), + TariffWriteable: eebusutil.Ptr(true), + UpdateRequired: eebusutil.Ptr(false), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeSimpleIncentiveTable), + }, + }, + }, + } + + rIncentiveFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeIncentiveTable, model.RoleTypeServer) + fErr = rIncentiveFeature.UpdateData(model.FunctionTypeIncentiveTableDescriptionData, incDesc, nil, nil) + assert.Nil(s.T(), fErr) + + // demand, No Profile No Timer demand + + timeData := &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(3)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(1)), + Value: model.NewScaledNumberType(0), + MaxValue: model.NewScaledNumberType(74690), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + demand, err := s.sut.EnergyDemand(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 0.0, demand.MinDemand) + assert.Equal(s.T(), 0.0, demand.OptDemand) + assert.Equal(s.T(), 74690.0, demand.MaxDemand) + assert.Equal(s.T(), 0.0, demand.DurationUntilStart) + assert.Equal(s.T(), 0.0, demand.DurationUntilEnd) + + // the final plan + + timeData = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(2)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(0)), + Duration: eebusutil.Ptr(model.DurationType("PT18H3M7S")), + MaxValue: model.NewScaledNumberType(4163), + }, + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(1)), + Duration: eebusutil.Ptr(model.DurationType("PT42M")), + MaxValue: model.NewScaledNumberType(2736), + }, + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(1)), + Duration: eebusutil.Ptr(model.DurationType("P1D")), + MaxValue: model.NewScaledNumberType(0), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + // demand, profile + timer with 80% target and no climate, minSoC reached + + timeData = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(3)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(1)), + Duration: eebusutil.Ptr(model.DurationType("P2DT4H40M36S")), + Value: model.NewScaledNumberType(53400), + MaxValue: model.NewScaledNumberType(74690), + }, + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(1)), + Duration: eebusutil.Ptr(model.DurationType("P1D")), + MaxValue: model.NewScaledNumberType(0), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + demand, err = s.sut.EnergyDemand(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 0.0, demand.MinDemand) + assert.Equal(s.T(), 53400.0, demand.OptDemand) + assert.Equal(s.T(), 74690.0, demand.MaxDemand) + assert.Equal(s.T(), 0.0, demand.DurationUntilStart) + assert.Equal(s.T(), time.Duration(time.Hour*52+time.Minute*40+time.Second*36).Seconds(), demand.DurationUntilEnd) + + // the final plan + + timeData = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(2)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(0)), + Duration: eebusutil.Ptr(model.DurationType("P1DT15H24M24S")), + MaxValue: model.NewScaledNumberType(0), + }, + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(1)), + Duration: eebusutil.Ptr(model.DurationType("PT12H35M50S")), + MaxValue: model.NewScaledNumberType(4163), + }, + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(2)), + Duration: eebusutil.Ptr(model.DurationType("PT40M22S")), + MaxValue: model.NewScaledNumberType(0), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + // demand, profile with 25% min SoC, minSoC not reached, no timer + + timeData = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(1)), + }, + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(2)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(0)), + Duration: eebusutil.Ptr(model.DurationType("PT8M42S")), + MaxValue: model.NewScaledNumberType(4212), + }, + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(1)), + Duration: eebusutil.Ptr(model.DurationType("P1D")), + MaxValue: model.NewScaledNumberType(0), + }, + }, + }, + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(3)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(1)), + Value: model.NewScaledNumberType(600), + MinValue: model.NewScaledNumberType(600), + MaxValue: model.NewScaledNumberType(75600), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + demand, err = s.sut.EnergyDemand(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 600.0, demand.MinDemand) + assert.Equal(s.T(), 600.0, demand.OptDemand) + assert.Equal(s.T(), 75600.0, demand.MaxDemand) + assert.Equal(s.T(), 0.0, demand.DurationUntilStart) + assert.Equal(s.T(), 0.0, demand.DurationUntilEnd) + + // the final plan + + timeData = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(2)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(0)), + Duration: eebusutil.Ptr(model.DurationType("PT8M42S")), + MaxValue: model.NewScaledNumberType(4212), + }, + { + TimeSeriesSlotId: eebusutil.Ptr(model.TimeSeriesSlotIdType(1)), + Duration: eebusutil.Ptr(model.DurationType("P1D")), + MaxValue: model.NewScaledNumberType(0), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) +} + +/* +func requestIncentiveUpdate(t *testing.T, datagram model.DatagramType, localDevice api.DeviceLocal, remoteDevice api.DeviceRemote) { + 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 api.DeviceLocal, remoteDevice api.DeviceRemote) { + 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/uccevc/testhelper_test.go b/uccevc/testhelper_test.go new file mode 100644 index 0000000..cce39bc --- /dev/null +++ b/uccevc/testhelper_test.go @@ -0,0 +1,195 @@ +package uccevc + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/cemd/api" + eebusapi "github.com/enbility/eebus-go/api" + eebusmocks "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestUCCEVCSuite(t *testing.T) { + suite.Run(t, new(UCCEVCSuite)) +} + +type UCCEVCSuite struct { + suite.Suite + + sut *UCCEVC + + service eebusapi.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *mocks.EntityRemoteInterface + evEntity spineapi.EntityRemoteInterface +} + +func (s *UCCEVCSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { +} + +func (s *UCCEVCSuite) BeforeTest(suiteName, testName string) { + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := eebusapi.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := eebusmocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := mocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = mocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := mocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + ops := map[model.FunctionType]spineapi.OperationsInterface{} + mockRemoteFeature.EXPECT().Operations().Return(ops).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + s.sut = NewUCCEVC(s.service, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + var entities []spineapi.EntityRemoteInterface + s.remoteDevice, entities = setupDevices(s.service, s.T()) + s.evEntity = entities[1] +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService eebusapi.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + []spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeDeviceConfiguration, + []model.FunctionType{ + model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, + model.FunctionTypeDeviceConfigurationKeyValueListData, + }, + }, + {model.FeatureTypeTypeTimeSeries, + []model.FunctionType{ + model.FunctionTypeTimeSeriesConstraintsListData, + model.FunctionTypeTimeSeriesDescriptionListData, + model.FunctionTypeTimeSeriesListData, + }, + }, + {model.FeatureTypeTypeIncentiveTable, + []model.FunctionType{ + model.FunctionTypeIncentiveTableConstraintsData, + model.FunctionTypeIncentiveTableDescriptionData, + model.FunctionTypeIncentiveTableData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + model.FunctionTypeElectricalConnectionPermittedValueSetListData, + }, + }, + } + + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: eebusutil.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + Feature: eebusutil.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: eebusutil.Ptr(feature.featureType), + Role: eebusutil.Ptr(model.RoleTypeServer), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeEVSE), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeEV), + }, + }, + }, + FeatureInformation: featureInformations, + } + + entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + remoteDevice.UpdateDevice(detailedData.DeviceInformation.Description) + + localDevice.AddRemoteDeviceForSki(remoteSki, remoteDevice) + + return remoteDevice, entities +} diff --git a/uccevc/types.go b/uccevc/types.go new file mode 100644 index 0000000..bc5c7f4 --- /dev/null +++ b/uccevc/types.go @@ -0,0 +1,46 @@ +package uccevc + +import "github.com/enbility/cemd/api" + +const ( + // Scenario 1 + + // EV provided an energy demand + // + // Use `EnergyDemand` to get the current data + DataUpdateEnergyDemand api.EventType = "DataUpdateEnergyDemand" + + // Scenario 2 + + // EV provided a charge plan constraints + // + // Use `TimeSlotConstraints` to get the current data + DataUpdateTimeSlotConstraints api.EventType = "DataUpdateTimeSlotConstraints" + + // Scenario 3 + + // EV incentive table data updated + // + // Use `IncentiveConstraints` to get the current data + DataUpdateIncentiveTable api.EventType = "DataUpdateIncentiveTable" + + // EV requested an incentive table, call to WriteIncentiveTableDescriptions required + DataRequestedIncentiveTableDescription api.EventType = "DataRequestedIncentiveTableDescription" + + // Scenario 2 & 3 + + // EV requested power limits, call to WritePowerLimits and WriteIncentives required + DataRequestedPowerLimitsAndIncentives api.EventType = "DataRequestedPowerLimitsAndIncentives" + + // Scenario 4 + + // EV provided a charge plan + // + // Use `ChargePlanConstraints` to get the current data + DataUpdateChargePlanConstraints api.EventType = "DataUpdateChargePlanConstraints" + + // EV provided a charge plan + // + // Use `ChargePlan` to get the current data + DataUpdateChargePlan api.EventType = "DataUpdateChargePlan" +) diff --git a/uccevc/uccevc.go b/uccevc/uccevc.go new file mode 100644 index 0000000..8f178f1 --- /dev/null +++ b/uccevc/uccevc.go @@ -0,0 +1,119 @@ +package uccevc + +import ( + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +type UCCEVC struct { + service eebusapi.ServiceInterface + + eventCB api.EntityEventCallback + + validEntityTypes []model.EntityTypeType +} + +var _ UCCEVCInterface = (*UCCEVC)(nil) + +func NewUCCEVC(service eebusapi.ServiceInterface, eventCB api.EntityEventCallback) *UCCEVC { + uc := &UCCEVC{ + service: service, + eventCB: eventCB, + } + + uc.validEntityTypes = []model.EntityTypeType{ + model.EntityTypeTypeEV, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (c *UCCEVC) UseCaseName() model.UseCaseNameType { + return model.UseCaseNameTypeCoordinatedEVCharging +} + +func (e *UCCEVC) AddFeatures() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceConfiguration, + model.FeatureTypeTypeTimeSeries, + model.FeatureTypeTypeIncentiveTable, + model.FeatureTypeTypeElectricalConnection, + } + for _, feature := range clientFeatures { + _ = localEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } +} + +func (e *UCCEVC) AddUseCase() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.AddUseCaseSupport( + model.UseCaseActorTypeCEM, + e.UseCaseName(), + model.SpecificationVersionType("1.0.1"), + "", + true, + []model.UseCaseScenarioSupportType{1, 2, 3}) +} + +func (e *UCCEVC) UpdateUseCaseAvailability(available bool) { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.SetUseCaseAvailability(model.UseCaseActorTypeCEM, e.UseCaseName(), available) +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *UCCEVC) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return false, api.ErrNoCompatibleEntity + } + + // check if the usecase and mandatory scenarios are supported and + // if the required server features are available + if !entity.Device().VerifyUseCaseScenariosAndFeaturesSupport( + model.UseCaseActorTypeEV, + e.UseCaseName(), + []model.UseCaseScenarioSupportType{2, 3, 4, 5, 6, 7, 8}, + []model.FeatureTypeType{ + model.FeatureTypeTypeTimeSeries, + model.FeatureTypeTypeIncentiveTable, + }, + ) { + return false, nil + } + + // check for required features + evTimeSeries, err := util.TimeSeries(e.service, entity) + if err != nil { + return false, eebusapi.ErrFunctionNotSupported + } + evIncentiveTable, err := util.IncentiveTable(e.service, entity) + if err != nil { + return false, eebusapi.ErrFunctionNotSupported + } + + // check if timeseries descriptions contains constraints data + if _, err = evTimeSeries.GetDescriptionForType(model.TimeSeriesTypeTypeConstraints); err != nil { + return false, err + } + + // check if incentive table descriptions contains data for the required scope + if _, err = evIncentiveTable.GetDescriptionsForScope(model.ScopeTypeTypeSimpleIncentiveTable); err != nil { + return false, err + } + + return true, nil +} diff --git a/uccevc/uccevc_test.go b/uccevc/uccevc_test.go new file mode 100644 index 0000000..400dba8 --- /dev/null +++ b/uccevc/uccevc_test.go @@ -0,0 +1,81 @@ +package uccevc + +import ( + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCCEVCSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *UCCEVCSuite) Test_IsUseCaseSupported() { + data, err := s.sut.IsUseCaseSupported(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + data, err = s.sut.IsUseCaseSupported(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: eebusutil.Ptr(model.UseCaseActorTypeEV), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: eebusutil.Ptr(model.UseCaseNameTypeCoordinatedEVCharging), + UseCaseAvailable: eebusutil.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{2, 3, 4, 5, 6, 7, 8}, + }, + }, + }, + }, + } + + nodemgmtEntity := s.remoteDevice.Entity([]model.AddressEntityType{0}) + nodeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(nodemgmtEntity, model.FeatureTypeTypeNodeManagement, model.RoleTypeSpecial) + fErr := nodeFeature.UpdateData(model.FunctionTypeNodeManagementUseCaseData, ucData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + timeDescData := &model.TimeSeriesDescriptionListDataType{ + TimeSeriesDescriptionData: []model.TimeSeriesDescriptionDataType{ + { + TimeSeriesId: eebusutil.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesType: eebusutil.Ptr(model.TimeSeriesTypeTypeConstraints), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeServer) + fErr = rFeature.UpdateData(model.FunctionTypeTimeSeriesDescriptionListData, timeDescData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + descData := &model.IncentiveTableDescriptionDataType{ + IncentiveTableDescription: []model.IncentiveTableDescriptionType{ + { + TariffDescription: &model.TariffDescriptionDataType{ + TariffId: eebusutil.Ptr(model.TariffIdType(0)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeSimpleIncentiveTable), + }, + }, + }, + } + + rFeature = s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeIncentiveTable, model.RoleTypeServer) + fErr = rFeature.UpdateData(model.FunctionTypeIncentiveTableDescriptionData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/ucevcc/api.go b/ucevcc/api.go new file mode 100644 index 0000000..83b24af --- /dev/null +++ b/ucevcc/api.go @@ -0,0 +1,78 @@ +package ucevcc + +import ( + "github.com/enbility/cemd/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +//go:generate mockery + +// interface for the EV Commissioning and Configuration UseCase +type UCEVCCInterface interface { + api.UseCaseInterface + + // return the current charge state of the EV + // + // parameters: + // - entity: the entity of the EV + ChargeState(entity spineapi.EntityRemoteInterface) (api.EVChargeStateType, error) + + // Scenario 1 & 8 + + // return if the EV is connected + // + // parameters: + // - entity: the entity of the EV + EVConnected(entity spineapi.EntityRemoteInterface) bool + + // Scenario 2 + + // return the current communication standard type used to communicate between EVSE and EV + // + // parameters: + // - entity: the entity of the EV + CommunicationStandard(entity spineapi.EntityRemoteInterface) (model.DeviceConfigurationKeyValueStringType, error) + + // Scenario 3 + + // return if the EV supports asymmetric charging + // + // parameters: + // - entity: the entity of the EV + AsymmetricChargingSupport(entity spineapi.EntityRemoteInterface) (bool, error) + + // Scenario 4 + + // return the identifications of the currently connected EV or nil if not available + // these can be multiple, e.g. PCID, Mac Address, RFID + // + // parameters: + // - entity: the entity of the EV + Identifications(entity spineapi.EntityRemoteInterface) ([]api.IdentificationItem, error) + + // Scenario 5 + + // the manufacturer data of an EVSE + // returns deviceName, serialNumber, error + // + // parameters: + // - entity: the entity of the EV + ManufacturerData(entity spineapi.EntityRemoteInterface) (api.ManufacturerData, error) + + // Scenario 6 + + // return the minimum, maximum charging and, standby power of the connected EV + // + // parameters: + // - entity: the entity of the EV + ChargingPowerLimits(entity spineapi.EntityRemoteInterface) (float64, float64, float64, error) + + // Scenario 7 + + // is the EV in sleep mode + // + // parameters: + // - entity: the entity of the EV + IsInSleepMode(entity spineapi.EntityRemoteInterface) (bool, error) +} diff --git a/ucevcc/events.go b/ucevcc/events.go new file mode 100644 index 0000000..fb0b783 --- /dev/null +++ b/ucevcc/events.go @@ -0,0 +1,215 @@ +package ucevcc + +import ( + "github.com/enbility/cemd/util" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// handle SPINE events +func (e *UCEVCC) HandleEvent(payload spineapi.EventPayload) { + // only about events from an EV entity or device changes for this remote device + + if !util.IsCompatibleEntity(payload.Entity, e.validEntityTypes) { + return + } + + if util.IsEntityConnected(payload) { + e.evConnected(payload) + return + } else if util.IsEntityDisconnected(payload) { + e.evDisconnected(payload) + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + + switch payload.Data.(type) { + case *model.DeviceConfigurationKeyValueDescriptionListDataType: + e.evConfigurationDescriptionDataUpdate(payload.Entity) + case *model.DeviceConfigurationKeyValueListDataType: + e.evConfigurationDataUpdate(payload) + case *model.DeviceDiagnosisOperatingStateType: + e.evOperatingStateDataUpdate(payload) + case *model.DeviceClassificationManufacturerDataType: + e.evManufacturerDataUpdate(payload) + case *model.ElectricalConnectionParameterDescriptionListDataType: + e.evElectricalParamerDescriptionUpdate(payload.Entity) + case *model.ElectricalConnectionPermittedValueSetListDataType: + e.evElectricalPermittedValuesUpdate(payload) + case *model.IdentificationListDataType: + e.evIdentificationDataUpdate(payload) + } +} + +// an EV was connected +func (e *UCEVCC) evConnected(payload spineapi.EventPayload) { + // initialise features, e.g. subscriptions, descriptions + if evDeviceClassification, err := util.DeviceClassification(e.service, payload.Entity); err == nil { + if _, err := evDeviceClassification.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + // get manufacturer details + if _, err := evDeviceClassification.RequestManufacturerDetails(); err != nil { + logging.Log().Debug(err) + } + } + + if evDeviceConfiguration, err := util.DeviceConfiguration(e.service, payload.Entity); err == nil { + if _, err := evDeviceConfiguration.Subscribe(); err != nil { + logging.Log().Debug(err) + } + // get ev configuration data + if _, err := evDeviceConfiguration.RequestDescriptions(); err != nil { + logging.Log().Debug(err) + } + } + + if evDeviceDiagnosis, err := util.DeviceDiagnosis(e.service, payload.Entity); err == nil { + if _, err := evDeviceDiagnosis.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + // get device diagnosis state + if _, err := evDeviceDiagnosis.RequestState(); err != nil { + logging.Log().Debug(err) + } + } + + if evElectricalConnection, err := util.ElectricalConnection(e.service, payload.Entity); err == nil { + if _, err := evElectricalConnection.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + // get electrical connection parameter descriptions + if _, err := evElectricalConnection.RequestParameterDescriptions(); err != nil { + logging.Log().Debug(err) + } + + // get electrical permitted values descriptions + if _, err := evElectricalConnection.RequestPermittedValueSets(); err != nil { + logging.Log().Debug(err) + } + } + + if evIdentification, err := util.Identification(e.service, payload.Entity); err == nil { + if _, err := evIdentification.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + // get identification + if _, err := evIdentification.RequestValues(); err != nil { + logging.Log().Debug(err) + } + } + + e.eventCB(payload.Ski, payload.Device, payload.Entity, EvConnected) +} + +// an EV was disconnected +func (e *UCEVCC) evDisconnected(payload spineapi.EventPayload) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, EvDisconnected) +} + +// the configuration key description data of an EV was updated +func (e *UCEVCC) evConfigurationDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if evDeviceConfiguration, err := util.DeviceConfiguration(e.service, entity); err == nil { + // key value descriptions received, now get the data + if _, err := evDeviceConfiguration.RequestKeyValues(); err != nil { + logging.Log().Error("Error getting configuration key values:", err) + } + } +} + +// the configuration key data of an EV was updated +func (e *UCEVCC) evConfigurationDataUpdate(payload spineapi.EventPayload) { + // Scenario 2 + if util.DeviceConfigurationCheckDataPayloadForKeyName(false, e.service, payload, model.DeviceConfigurationKeyNameTypeCommunicationsStandard) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateCommunicationStandard) + } + + // Scenario 3 + if util.DeviceConfigurationCheckDataPayloadForKeyName(false, e.service, payload, model.DeviceConfigurationKeyNameTypeAsymmetricChargingSupported) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateAsymmetricChargingSupport) + } +} + +// the operating state of an EV was updated +func (e *UCEVCC) evOperatingStateDataUpdate(payload spineapi.EventPayload) { + deviceDiagnosis, err := util.DeviceDiagnosis(e.service, payload.Entity) + if err != nil { + return + } + + if _, err := deviceDiagnosis.GetState(); err == nil { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateIdentifications) + } +} + +// the identification data of an EV was updated +func (e *UCEVCC) evIdentificationDataUpdate(payload spineapi.EventPayload) { + evIdentification, err := util.Identification(e.service, payload.Entity) + if err != nil { + return + } + + // Scenario 4 + if values, err := evIdentification.GetValues(); err == nil { + for _, item := range values { + if item.IdentificationId == nil || item.IdentificationValue == nil { + continue + } + + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateIdentifications) + return + } + } +} + +// the manufacturer data of an EV was updated +func (e *UCEVCC) evManufacturerDataUpdate(payload spineapi.EventPayload) { + evDeviceClassification, err := util.DeviceClassification(e.service, payload.Entity) + if err != nil { + return + } + + // Scenario 5 + if _, err := evDeviceClassification.GetManufacturerDetails(); err == nil { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateManufacturerData) + } +} + +// the electrical connection parameter description data of an EV was updated +func (e *UCEVCC) evElectricalParamerDescriptionUpdate(entity spineapi.EntityRemoteInterface) { + if evElectricalConnection, err := util.ElectricalConnection(e.service, entity); err == nil { + if _, err := evElectricalConnection.RequestPermittedValueSets(); err != nil { + logging.Log().Error("Error getting electrical permitted values:", err) + } + } +} + +// the electrical connection permitted value sets data of an EV was updated +func (e *UCEVCC) evElectricalPermittedValuesUpdate(payload spineapi.EventPayload) { + evElectricalConnection, err := util.ElectricalConnection(e.service, payload.Entity) + if err != nil { + return + } + + data, err := evElectricalConnection.GetParameterDescriptionForScopeType(model.ScopeTypeTypeACPowerTotal) + if err != nil || data.ParameterId == nil { + return + } + + values, err := evElectricalConnection.GetPermittedValueSetForParameterId(*data.ParameterId) + if err != nil || values == nil { + return + } + + // Scenario 6 + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateCurrentLimits) +} diff --git a/ucevcc/events_test.go b/ucevcc/events_test.go new file mode 100644 index 0000000..d569c40 --- /dev/null +++ b/ucevcc/events_test.go @@ -0,0 +1,269 @@ +package ucevcc + +import ( + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCEVCCSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.evEntity + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDeviceChange + payload.ChangeType = spineapi.ElementChangeRemove + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeEntityChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeEntityChange + payload.ChangeType = spineapi.ElementChangeRemove + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.Data = eebusutil.Ptr(model.DeviceConfigurationKeyValueDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.DeviceConfigurationKeyValueListDataType{}) + s.sut.HandleEvent(payload) + + var value model.DeviceDiagnosisOperatingStateType + payload.Data = &value + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.DeviceClassificationManufacturerDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.ElectricalConnectionParameterDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.ElectricalConnectionPermittedValueSetListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.IdentificationListDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *UCEVCCSuite) Test_Failures() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.evConnected(payload) + + s.sut.evConfigurationDescriptionDataUpdate(s.mockRemoteEntity) + + s.sut.evElectricalParamerDescriptionUpdate(s.mockRemoteEntity) +} + +func (s *UCEVCCSuite) Test_evConfigurationDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evConfigurationDataUpdate(payload) + assert.False(s.T(), s.eventCBInvoked) + + payload.Entity = s.evEntity + s.sut.evConfigurationDataUpdate(payload) + assert.False(s.T(), s.eventCBInvoked) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(1)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeCommunicationsStandard), + }, + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(2)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeAsymmetricChargingSupported), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evConfigurationDataUpdate(payload) + assert.False(s.T(), s.eventCBInvoked) + + data := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{}, + } + + payload.Data = data + + s.sut.evConfigurationDataUpdate(payload) + assert.False(s.T(), s.eventCBInvoked) + + data = &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: eebusutil.Ptr(model.DeviceConfigurationKeyValueValueType{}), + }, + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(1)), + Value: eebusutil.Ptr(model.DeviceConfigurationKeyValueValueType{ + String: eebusutil.Ptr(model.DeviceConfigurationKeyValueStringTypeISO151182ED2), + }), + }, + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(2)), + Value: eebusutil.Ptr(model.DeviceConfigurationKeyValueValueType{ + Boolean: eebusutil.Ptr(false), + }), + }, + }, + } + + payload.Data = data + + s.sut.evConfigurationDataUpdate(payload) + assert.True(s.T(), s.eventCBInvoked) +} + +func (s *UCEVCCSuite) Test_evOperatingStateDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evOperatingStateDataUpdate(payload) + assert.False(s.T(), s.eventCBInvoked) + + payload.Entity = s.evEntity + s.sut.evOperatingStateDataUpdate(payload) + assert.False(s.T(), s.eventCBInvoked) + + data := &model.DeviceDiagnosisStateDataType{ + OperatingState: eebusutil.Ptr(model.DeviceDiagnosisOperatingStateTypeNormalOperation), + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceDiagnosisStateData, data, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evOperatingStateDataUpdate(payload) + assert.True(s.T(), s.eventCBInvoked) +} + +func (s *UCEVCCSuite) Test_evIdentificationDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evIdentificationDataUpdate(payload) + assert.False(s.T(), s.eventCBInvoked) + + payload.Entity = s.evEntity + s.sut.evIdentificationDataUpdate(payload) + assert.False(s.T(), s.eventCBInvoked) + + data := &model.IdentificationListDataType{ + IdentificationData: []model.IdentificationDataType{ + { + IdentificationId: eebusutil.Ptr(model.IdentificationIdType(0)), + IdentificationType: eebusutil.Ptr(model.IdentificationTypeTypeEui48), + }, + { + IdentificationId: eebusutil.Ptr(model.IdentificationIdType(1)), + IdentificationType: eebusutil.Ptr(model.IdentificationTypeTypeEui48), + IdentificationValue: eebusutil.Ptr(model.IdentificationValueType("test")), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeIdentification, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeIdentificationListData, data, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evIdentificationDataUpdate(payload) + assert.True(s.T(), s.eventCBInvoked) +} + +func (s *UCEVCCSuite) Test_evManufacturerDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evManufacturerDataUpdate(payload) + assert.False(s.T(), s.eventCBInvoked) + + payload.Entity = s.evEntity + s.sut.evManufacturerDataUpdate(payload) + assert.False(s.T(), s.eventCBInvoked) + + data := &model.DeviceClassificationManufacturerDataType{ + BrandName: eebusutil.Ptr(model.DeviceClassificationStringType("test")), + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeDeviceClassification, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceClassificationManufacturerData, data, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evManufacturerDataUpdate(payload) + assert.True(s.T(), s.eventCBInvoked) +} + +func (s *UCEVCCSuite) Test_evElectricalPermittedValuesUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evElectricalPermittedValuesUpdate(payload) + assert.False(s.T(), s.eventCBInvoked) + + payload.Entity = s.evEntity + s.sut.evElectricalPermittedValuesUpdate(payload) + assert.False(s.T(), s.eventCBInvoked) + + paramData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(0)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evElectricalPermittedValuesUpdate(payload) + assert.False(s.T(), s.eventCBInvoked) + + permData := &model.ElectricalConnectionPermittedValueSetListDataType{ + ElectricalConnectionPermittedValueSetData: []model.ElectricalConnectionPermittedValueSetDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(0)), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeElectricalConnectionPermittedValueSetListData, permData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evElectricalPermittedValuesUpdate(payload) + assert.True(s.T(), s.eventCBInvoked) +} diff --git a/ucevcc/public.go b/ucevcc/public.go new file mode 100644 index 0000000..b653dec --- /dev/null +++ b/ucevcc/public.go @@ -0,0 +1,275 @@ +package ucevcc + +import ( + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// return the current charge state of the EV +func (e *UCEVCC) ChargeState(entity spineapi.EntityRemoteInterface) (api.EVChargeStateType, error) { + if entity == nil || entity.EntityType() != model.EntityTypeTypeEV { + return api.EVChargeStateTypeUnplugged, nil + } + + evDeviceDiagnosis, err := util.DeviceDiagnosis(e.service, entity) + if err != nil { + return api.EVChargeStateTypeUnplugged, err + } + + diagnosisState, err := evDeviceDiagnosis.GetState() + if err != nil { + return api.EVChargeStateTypeUnknown, err + } + + operatingState := diagnosisState.OperatingState + if operatingState == nil { + return api.EVChargeStateTypeUnknown, eebusapi.ErrDataNotAvailable + } + + switch *operatingState { + case model.DeviceDiagnosisOperatingStateTypeNormalOperation: + return api.EVChargeStateTypeActive, nil + case model.DeviceDiagnosisOperatingStateTypeStandby: + return api.EVChargeStateTypePaused, nil + case model.DeviceDiagnosisOperatingStateTypeFailure: + return api.EVChargeStateTypeError, nil + case model.DeviceDiagnosisOperatingStateTypeFinished: + return api.EVChargeStateTypeFinished, nil + } + + return api.EVChargeStateTypeUnknown, nil +} + +// return if an EV is connected +// +// this includes all required features and +// minimal data being available +func (e *UCEVCC) EVConnected(entity spineapi.EntityRemoteInterface) bool { + if entity == nil || entity.Device() == nil { + return false + } + + // getting current charge state should work + if _, err := e.ChargeState(entity); err != nil { + return false + } + + remoteDevice := e.service.LocalDevice().RemoteDeviceForSki(entity.Device().Ski()) + if remoteDevice == nil { + return false + } + + // check if the device still has an entity assigned with the provided entities address + return remoteDevice.Entity(entity.Address().Entity) == entity +} + +func (e *UCEVCC) deviceConfigurationValueForKeyName( + entity spineapi.EntityRemoteInterface, + keyname model.DeviceConfigurationKeyNameType, + valueType model.DeviceConfigurationKeyValueTypeType) (any, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return nil, api.ErrNoCompatibleEntity + } + + evDeviceConfiguration, err := util.DeviceConfiguration(e.service, entity) + if err != nil { + return nil, eebusapi.ErrDataNotAvailable + } + + // check if device configuration descriptions has an communication standard key name + _, err = evDeviceConfiguration.GetDescriptionForKeyName(keyname) + if err != nil { + return nil, err + } + + data, err := evDeviceConfiguration.GetKeyValueForKeyName(keyname, valueType) + if err != nil { + return nil, err + } + + if data == nil { + return nil, eebusapi.ErrDataNotAvailable + } + + return data, nil +} + +// return the current communication standard type used to communicate between EVSE and EV +// +// if an EV is connected via IEC61851, no ISO15118 specific data can be provided! +// sometimes the connection starts with IEC61851 before it switches +// to ISO15118, and sometimes it falls back again. so the error return is +// never absolut for the whole connection time, except if the use case +// is not supported +// +// the values are not constant and can change due to communication problems, bugs, and +// sometimes communication starts with IEC61851 before it switches to ISO +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *UCEVCC) CommunicationStandard(entity spineapi.EntityRemoteInterface) (model.DeviceConfigurationKeyValueStringType, error) { + unknown := UCEVCCCommunicationStandardUnknown + + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return unknown, api.ErrNoCompatibleEntity + } + + data, err := e.deviceConfigurationValueForKeyName(entity, model.DeviceConfigurationKeyNameTypeCommunicationsStandard, model.DeviceConfigurationKeyValueTypeTypeString) + if err != nil || data == nil { + return unknown, eebusapi.ErrDataNotAvailable + } + + value, ok := data.(*model.DeviceConfigurationKeyValueStringType) + if !ok || value == nil { + return unknown, eebusapi.ErrDataNotAvailable + } + + return *value, nil +} + +// return if the EV supports asymmetric charging +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +func (e *UCEVCC) AsymmetricChargingSupport(entity spineapi.EntityRemoteInterface) (bool, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return false, api.ErrNoCompatibleEntity + } + + data, err := e.deviceConfigurationValueForKeyName(entity, model.DeviceConfigurationKeyNameTypeAsymmetricChargingSupported, model.DeviceConfigurationKeyValueTypeTypeBoolean) + if err != nil || data == nil { + return false, eebusapi.ErrDataNotAvailable + } + + value, ok := data.(*bool) + if !ok || value == nil { + return false, eebusapi.ErrDataNotAvailable + } + + return bool(*value), nil +} + +// return the identifications of the currently connected EV or nil if not available +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *UCEVCC) Identifications(entity spineapi.EntityRemoteInterface) ([]api.IdentificationItem, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return nil, api.ErrNoCompatibleEntity + } + + evIdentification, err := util.Identification(e.service, entity) + if err != nil { + return nil, eebusapi.ErrDataNotAvailable + } + + identifications, err := evIdentification.GetValues() + if err != nil { + return nil, err + } + + var ids []api.IdentificationItem + for _, identification := range identifications { + item := api.IdentificationItem{} + + typ := identification.IdentificationType + if typ != nil { + item.ValueType = *typ + } + + value := identification.IdentificationValue + if value != nil { + item.Value = string(*value) + } + + ids = append(ids, item) + } + + return ids, nil +} + +// the manufacturer data of an EVSE +// returns deviceName, serialNumber, error +func (e *UCEVCC) ManufacturerData( + entity spineapi.EntityRemoteInterface, +) ( + api.ManufacturerData, + error, +) { + return util.ManufacturerData(e.service, entity, e.validEntityTypes) +} + +// return the minimum, maximum charging and, standby power of the connected EV +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (e *UCEVCC) ChargingPowerLimits(entity spineapi.EntityRemoteInterface) (float64, float64, float64, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0.0, 0.0, 0.0, api.ErrNoCompatibleEntity + } + + evElectricalConnection, err := util.ElectricalConnection(e.service, entity) + if err != nil { + return 0.0, 0.0, 0.0, eebusapi.ErrDataNotAvailable + } + + elParamDesc, err := evElectricalConnection.GetParameterDescriptionForScopeType(model.ScopeTypeTypeACPowerTotal) + if err != nil || elParamDesc.ParameterId == nil { + return 0.0, 0.0, 0.0, eebusapi.ErrDataNotAvailable + } + + dataSet, err := evElectricalConnection.GetPermittedValueSetForParameterId(*elParamDesc.ParameterId) + if err != nil || dataSet == nil || + dataSet.PermittedValueSet == nil || + len(dataSet.PermittedValueSet) != 1 || + dataSet.PermittedValueSet[0].Range == nil || + len(dataSet.PermittedValueSet[0].Range) != 1 { + return 0.0, 0.0, 0.0, eebusapi.ErrDataNotAvailable + } + + var minValue, maxValue, standByValue float64 + if dataSet.PermittedValueSet[0].Range[0].Min != nil { + minValue = dataSet.PermittedValueSet[0].Range[0].Min.GetValue() + } + if dataSet.PermittedValueSet[0].Range[0].Max != nil { + maxValue = dataSet.PermittedValueSet[0].Range[0].Max.GetValue() + } + if dataSet.PermittedValueSet[0].Value != nil && len(dataSet.PermittedValueSet[0].Value) > 0 { + standByValue = dataSet.PermittedValueSet[0].Value[0].GetValue() + } + + return minValue, maxValue, standByValue, nil +} + +// is the EV in sleep mode +// returns operatingState, lastErrorCode, error +func (e *UCEVCC) IsInSleepMode( + entity spineapi.EntityRemoteInterface, +) (bool, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return false, api.ErrNoCompatibleEntity + } + + evseDeviceDiagnosis, err := util.DeviceDiagnosis(e.service, entity) + if err != nil { + return false, err + } + + data, err := evseDeviceDiagnosis.GetState() + if err != nil { + return false, err + } + + if data.OperatingState != nil && + *data.OperatingState == model.DeviceDiagnosisOperatingStateTypeStandby { + return true, nil + } + + return false, nil +} diff --git a/ucevcc/public_test.go b/ucevcc/public_test.go new file mode 100644 index 0000000..8ff39dd --- /dev/null +++ b/ucevcc/public_test.go @@ -0,0 +1,420 @@ +package ucevcc + +import ( + "testing" + + "github.com/enbility/cemd/api" + "github.com/enbility/eebus-go/util" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCEVCCSuite) Test_ChargeState() { + data, err := s.sut.ChargeState(s.mockRemoteEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), api.EVChargeStateTypeUnplugged, data) + + data, err = s.sut.ChargeState(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), api.EVChargeStateTypeUnknown, data) + + stateData := &model.DeviceDiagnosisStateDataType{ + OperatingState: util.Ptr(model.DeviceDiagnosisOperatingStateTypeNormalOperation), + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceDiagnosisStateData, stateData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.ChargeState(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), api.EVChargeStateTypeActive, data) + + stateData = &model.DeviceDiagnosisStateDataType{ + OperatingState: util.Ptr(model.DeviceDiagnosisOperatingStateTypeStandby), + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceDiagnosisStateData, stateData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.ChargeState(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), api.EVChargeStateTypePaused, data) + + stateData = &model.DeviceDiagnosisStateDataType{ + OperatingState: util.Ptr(model.DeviceDiagnosisOperatingStateTypeFailure), + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceDiagnosisStateData, stateData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.ChargeState(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), api.EVChargeStateTypeError, data) + + stateData = &model.DeviceDiagnosisStateDataType{ + OperatingState: util.Ptr(model.DeviceDiagnosisOperatingStateTypeFinished), + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceDiagnosisStateData, stateData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.ChargeState(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), api.EVChargeStateTypeFinished, data) + + stateData = &model.DeviceDiagnosisStateDataType{ + OperatingState: util.Ptr(model.DeviceDiagnosisOperatingStateTypeInAlarm), + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceDiagnosisStateData, stateData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.ChargeState(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), api.EVChargeStateTypeUnknown, data) +} + +func (s *UCEVCCSuite) Test_EVConnected() { + data := s.sut.EVConnected(nil) + assert.Equal(s.T(), false, data) + + data = s.sut.EVConnected(s.mockRemoteEntity) + assert.Equal(s.T(), false, data) + + data = s.sut.EVConnected(s.evEntity) + assert.Equal(s.T(), false, data) + + stateData := &model.DeviceDiagnosisStateDataType{ + OperatingState: util.Ptr(model.DeviceDiagnosisOperatingStateTypeNormalOperation), + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceDiagnosisStateData, stateData, nil, nil) + assert.Nil(s.T(), fErr) + + data = s.sut.EVConnected(s.evEntity) + assert.Equal(s.T(), true, data) +} + +func (s *UCEVCCSuite) Test_EVCommunicationStandard() { + data, err := s.sut.CommunicationStandard(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), UCEVCCCommunicationStandardUnknown, data) + + data, err = s.sut.CommunicationStandard(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), UCEVCCCommunicationStandardUnknown, data) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeAsymmetricChargingSupported), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.CommunicationStandard(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), UCEVCCCommunicationStandardUnknown, data) + + descData = &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeCommunicationsStandard), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.CommunicationStandard(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), UCEVCCCommunicationStandardUnknown, data) + + devData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + String: eebusutil.Ptr(model.DeviceConfigurationKeyValueStringTypeISO151182ED2), + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, devData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.CommunicationStandard(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), model.DeviceConfigurationKeyValueStringTypeISO151182ED2, data) +} + +func (s *UCEVCCSuite) Test_EVAsymmetricChargingSupport() { + data, err := s.sut.AsymmetricChargingSupport(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + data, err = s.sut.AsymmetricChargingSupport(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeAsymmetricChargingSupported), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.AsymmetricChargingSupport(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + descData = &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeAsymmetricChargingSupported), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.AsymmetricChargingSupport(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + devData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + Boolean: eebusutil.Ptr(true), + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, devData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.AsymmetricChargingSupport(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} + +func (s *UCEVCCSuite) Test_EVIdentification() { + data, err := s.sut.Identifications(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), []api.IdentificationItem(nil), data) + + data, err = s.sut.Identifications(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), []api.IdentificationItem(nil), data) + + data, err = s.sut.Identifications(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), []api.IdentificationItem(nil), data) + + idData := &model.IdentificationListDataType{ + IdentificationData: []model.IdentificationDataType{ + { + IdentificationId: util.Ptr(model.IdentificationIdType(0)), + IdentificationType: util.Ptr(model.IdentificationTypeTypeEui64), + IdentificationValue: util.Ptr(model.IdentificationValueType("test")), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeIdentification, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeIdentificationListData, idData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Identifications(s.evEntity) + assert.Nil(s.T(), err) + resultData := []api.IdentificationItem{{Value: "test", ValueType: model.IdentificationTypeTypeEui64}} + assert.Equal(s.T(), resultData, data) +} + +func (s *UCEVCCSuite) Test_EVManufacturerData() { + _, err := s.sut.ManufacturerData(nil) + assert.NotNil(s.T(), err) + + _, err = s.sut.ManufacturerData(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.ManufacturerData(s.evEntity) + assert.NotNil(s.T(), err) + + descData := &model.DeviceClassificationManufacturerDataType{} + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeDeviceClassification, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceClassificationManufacturerData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err := s.sut.ManufacturerData(s.evEntity) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), data) + assert.Equal(s.T(), "", data.DeviceName) + assert.Equal(s.T(), "", data.SerialNumber) + + descData = &model.DeviceClassificationManufacturerDataType{ + DeviceName: eebusutil.Ptr(model.DeviceClassificationStringType("test")), + SerialNumber: eebusutil.Ptr(model.DeviceClassificationStringType("12345")), + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceClassificationManufacturerData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.ManufacturerData(s.evEntity) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), data) + assert.Equal(s.T(), "test", data.DeviceName) + assert.Equal(s.T(), "12345", data.SerialNumber) + assert.Equal(s.T(), "", data.BrandName) +} + +func (s *UCEVCCSuite) Test_EVChargingPowerLimits() { + minData, maxData, standByData, err := s.sut.ChargingPowerLimits(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, minData) + assert.Equal(s.T(), 0.0, maxData) + assert.Equal(s.T(), 0.0, standByData) + + minData, maxData, standByData, err = s.sut.ChargingPowerLimits(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, minData) + assert.Equal(s.T(), 0.0, maxData) + assert.Equal(s.T(), 0.0, standByData) + + paramData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramData, nil, nil) + assert.Nil(s.T(), fErr) + + minData, maxData, standByData, err = s.sut.ChargingPowerLimits(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, minData) + assert.Equal(s.T(), 0.0, maxData) + assert.Equal(s.T(), 0.0, standByData) + + type permittedStruct struct { + standByValue, expectedStandByValue float64 + minValue, expectedMinValue float64 + maxValue, expectedMaxValue float64 + } + + tests := []struct { + name string + permitted permittedStruct + }{ + { + "IEC 3 Phase", + permittedStruct{0.1, 0.1, 4287600, 4287600, 11433600, 11433600}, + }, + { + "ISO15118 VW", + permittedStruct{0.1, 0.1, 800, 800, 11433600, 11433600}, + }, + { + "ISO15118 Taycan", + permittedStruct{0.1, 0.1, 400, 400, 11433600, 11433600}, + }, + } + + for _, tc := range tests { + s.T().Run(tc.name, func(t *testing.T) { + dataSet := []model.ElectricalConnectionPermittedValueSetDataType{} + permittedData := []model.ScaledNumberSetType{} + item := model.ScaledNumberSetType{ + Range: []model.ScaledNumberRangeType{ + { + Min: model.NewScaledNumberType(tc.permitted.minValue), + Max: model.NewScaledNumberType(tc.permitted.maxValue), + }, + }, + Value: []model.ScaledNumberType{*model.NewScaledNumberType(tc.permitted.standByValue)}, + } + permittedData = append(permittedData, item) + + permittedItem := model.ElectricalConnectionPermittedValueSetDataType{ + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + PermittedValueSet: permittedData, + } + dataSet = append(dataSet, permittedItem) + + permData := &model.ElectricalConnectionPermittedValueSetListDataType{ + ElectricalConnectionPermittedValueSetData: dataSet, + } + + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionPermittedValueSetListData, permData, nil, nil) + assert.Nil(s.T(), fErr) + + minData, maxData, standByData, err = s.sut.ChargingPowerLimits(s.evEntity) + assert.Nil(s.T(), err) + + assert.Nil(s.T(), err) + assert.Equal(s.T(), tc.permitted.expectedMinValue, minData) + assert.Equal(s.T(), tc.permitted.expectedMaxValue, maxData) + assert.Equal(s.T(), tc.permitted.expectedStandByValue, standByData) + }) + } +} + +func (s *UCEVCCSuite) Test_EVInSleepMode() { + data, err := s.sut.IsInSleepMode(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + data, err = s.sut.IsInSleepMode(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + descData := &model.DeviceDiagnosisStateDataType{} + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceDiagnosisStateData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsInSleepMode(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + descData = &model.DeviceDiagnosisStateDataType{ + OperatingState: eebusutil.Ptr(model.DeviceDiagnosisOperatingStateTypeStandby), + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceDiagnosisStateData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsInSleepMode(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/ucevcc/results.go b/ucevcc/results.go new file mode 100644 index 0000000..4a6b237 --- /dev/null +++ b/ucevcc/results.go @@ -0,0 +1,61 @@ +package ucevcc + +import ( + "fmt" + + "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +func (e *UCEVCC) HandleResponse(responseMsg api.ResponseMessage) { + // before SPINE 1.3 the heartbeats are on the EVSE entity + if responseMsg.EntityRemote == nil || + (responseMsg.EntityRemote.EntityType() != model.EntityTypeTypeEV && + responseMsg.EntityRemote.EntityType() != model.EntityTypeTypeEVSE) { + return + } + + // handle errors coming from the remote EVSE entity + if responseMsg.FeatureLocal.Type() == model.FeatureTypeTypeDeviceDiagnosis { + e.handleResultDeviceDiagnosis(responseMsg) + } +} + +// Handle DeviceDiagnosis Results +func (e *UCEVCC) handleResultDeviceDiagnosis(responseMsg api.ResponseMessage) { + // is this an error for a heartbeat message? + if responseMsg.DeviceRemote == nil || + responseMsg.Data == nil { + return + } + + var result *model.ResultDataType + + switch responseMsg.Data.(type) { + case *model.ResultDataType: + result = responseMsg.Data.(*model.ResultDataType) + default: + return + } + + if result.ErrorNumber == nil || + *result.ErrorNumber == model.ErrorNumberTypeNoError { + return + } + + // check if this is for a cached notify message + datagram, err := responseMsg.DeviceRemote.Sender().DatagramForMsgCounter(responseMsg.MsgCounterReference) + if err != nil { + return + } + + if len(datagram.Payload.Cmd) > 0 && + datagram.Payload.Cmd[0].DeviceDiagnosisHeartbeatData != nil { + // something is horribly wrong, disconnect and hope a new connection will fix it + errorText := fmt.Sprintf("Error Code: %d", result.ErrorNumber) + if result.Description != nil { + errorText = fmt.Sprintf("%s - %s", errorText, string(*result.Description)) + } + e.service.DisconnectSKI(responseMsg.DeviceRemote.Ski(), errorText) + } +} diff --git a/ucevcc/results_test.go b/ucevcc/results_test.go new file mode 100644 index 0000000..11a93bc --- /dev/null +++ b/ucevcc/results_test.go @@ -0,0 +1,72 @@ +package ucevcc + +import ( + "errors" + + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +func (s *UCEVCCSuite) Test_Results() { + localDevice := s.service.LocalDevice() + localEntity := localDevice.EntityForType(model.EntityTypeTypeCEM) + localFeature := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeClient) + + errorMsg := spineapi.ResponseMessage{ + DeviceRemote: s.remoteDevice, + EntityRemote: s.evEntity, + FeatureLocal: localFeature, + Data: eebusutil.Ptr(model.MsgCounterType(0)), + } + s.sut.HandleResponse(errorMsg) + + errorMsg = spineapi.ResponseMessage{ + EntityRemote: s.evEntity, + FeatureLocal: localFeature, + Data: eebusutil.Ptr(model.MsgCounterType(0)), + } + s.sut.HandleResponse(errorMsg) + + errorMsg = spineapi.ResponseMessage{ + DeviceRemote: s.remoteDevice, + EntityRemote: s.mockRemoteEntity, + FeatureLocal: localFeature, + Data: &model.ResultDataType{ + ErrorNumber: eebusutil.Ptr(model.ErrorNumberTypeNoError), + }, + } + s.sut.HandleResponse(errorMsg) + + errorMsg.EntityRemote = s.evEntity + s.sut.HandleResponse(errorMsg) + + errorMsg.Data = &model.ResultDataType{ + ErrorNumber: eebusutil.Ptr(model.ErrorNumberTypeGeneralError), + Description: eebusutil.Ptr(model.DescriptionType("test error")), + } + errorMsg.MsgCounterReference = model.MsgCounterType(500) + + s.mockSender. + EXPECT(). + DatagramForMsgCounter(errorMsg.MsgCounterReference). + Return(model.DatagramType{}, errors.New("test")).Once() + + s.sut.HandleResponse(errorMsg) + + datagram := model.DatagramType{ + Payload: model.PayloadType{ + Cmd: []model.CmdType{ + { + DeviceDiagnosisHeartbeatData: &model.DeviceDiagnosisHeartbeatDataType{}, + }, + }, + }, + } + s.mockSender. + EXPECT(). + DatagramForMsgCounter(errorMsg.MsgCounterReference). + Return(datagram, nil).Once() + + s.sut.HandleResponse(errorMsg) +} diff --git a/ucevcc/testhelper_test.go b/ucevcc/testhelper_test.go new file mode 100644 index 0000000..6461b7b --- /dev/null +++ b/ucevcc/testhelper_test.go @@ -0,0 +1,210 @@ +package ucevcc + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/cemd/api" + eebusapi "github.com/enbility/eebus-go/api" + eebusmocks "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/ship-go/cert" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestEVCCSuite(t *testing.T) { + suite.Run(t, new(UCEVCCSuite)) +} + +type UCEVCCSuite struct { + suite.Suite + + sut *UCEVCC + + service eebusapi.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockSender *mocks.SenderInterface + mockRemoteEntity *mocks.EntityRemoteInterface + evEntity spineapi.EntityRemoteInterface + + eventCBInvoked bool +} + +func (s *UCEVCCSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + s.eventCBInvoked = true +} + +func (s *UCEVCCSuite) BeforeTest(suiteName, testName string) { + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := eebusapi.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := eebusmocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := mocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = mocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := mocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + s.eventCBInvoked = false + + s.sut = NewUCEVCC(s.service, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + var entities []spineapi.EntityRemoteInterface + s.remoteDevice, s.mockSender, entities = setupDevices(s.service, s.T()) + s.evEntity = entities[1] +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService eebusapi.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + *mocks.SenderInterface, + []spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + mockSender := mocks.NewSenderInterface(t) + defaultMsgCounter := model.MsgCounterType(100) + mockSender. + EXPECT(). + Request(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(&defaultMsgCounter, nil). + Maybe() + mockSender. + EXPECT(). + Subscribe(mock.Anything, mock.Anything, mock.Anything). + Return(&defaultMsgCounter, nil). + Maybe() + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, mockSender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeDeviceConfiguration, + []model.FunctionType{ + model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, + model.FunctionTypeDeviceConfigurationKeyValueListData, + }, + }, + {model.FeatureTypeTypeIdentification, + []model.FunctionType{ + model.FunctionTypeIdentificationListData, + }, + }, + {model.FeatureTypeTypeDeviceClassification, + []model.FunctionType{ + model.FunctionTypeDeviceClassificationManufacturerData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionDescriptionListData, + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + model.FunctionTypeElectricalConnectionPermittedValueSetListData, + }, + }, + {model.FeatureTypeTypeDeviceDiagnosis, + []model.FunctionType{ + model.FunctionTypeDeviceDiagnosisStateData, + }, + }, + } + + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: eebusutil.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + Feature: eebusutil.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: eebusutil.Ptr(feature.featureType), + Role: eebusutil.Ptr(model.RoleTypeServer), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeEVSE), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeEV), + }, + }, + }, + FeatureInformation: featureInformations, + } + + entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + remoteDevice.UpdateDevice(detailedData.DeviceInformation.Description) + + localDevice.AddRemoteDeviceForSki(remoteSki, remoteDevice) + + return remoteDevice, mockSender, entities +} diff --git a/ucevcc/types.go b/ucevcc/types.go new file mode 100644 index 0000000..bc89a83 --- /dev/null +++ b/ucevcc/types.go @@ -0,0 +1,71 @@ +package ucevcc + +import ( + "github.com/enbility/cemd/api" + "github.com/enbility/spine-go/model" +) + +// value if the UCEVCC communication standard is unknown +const ( + UCEVCCCommunicationStandardUnknown model.DeviceConfigurationKeyValueStringType = "unknown" +) + +const ( + + // An EV was connected + // + // Use Case EVCC, Scenario 1 + EvConnected api.EventType = "EvConnected" + + // An EV was disconnected + // + // Note: The ev entity is no longer connected to the device! + // + // Use Case EVCC, Scenario 8 + EvDisconnected api.EventType = "EvDisconnected" + + // EV charge state data was updated + // + // Use `ChargeState` to get the current data + DataUpdateChargeState api.EventType = "DataUpdateChargeState" + + // EV communication standard data was updated + // + // Use `CommunicationStandard` to get the current data + // + // Use Case EVCC, Scenario 2 + DataUpdateCommunicationStandard api.EventType = "DataUpdateCommunicationStandard" + + // EV asymmetric charging data was updated + // + // Use `AsymmetricChargingSupport` to get the current data + DataUpdateAsymmetricChargingSupport api.EventType = "DataUpdateAsymmetricChargingSupport" + + // EV identificationdata was updated + // + // Use `Identifications` to get the current data + // + // Use Case EVCC, Scenario 4 + DataUpdateIdentifications api.EventType = "DataUpdateIdentifications" + + // EV manufacturer data was updated + // + // Use `ManufacturerData` to get the current data + // + // Use Case EVCC, Scenario 5 + DataUpdateManufacturerData api.EventType = "DataUpdateManufacturerData" + + // EV charging power limits + // + // Use `ChargingPowerLimits` to get the current data + // + // Use Case EVCC, Scenario 6 + DataUpdateCurrentLimits api.EventType = "DataUpdateCurrentLimits" + + // EV permitted power limits updated + // + // Use `IsInSleepMode` to get the current data + // + // Use Case EVCC, Scenario 7 + DataUpdateIsInSleepMode api.EventType = "DataUpdateIsInSleepMode" +) diff --git a/ucevcc/ucevcc.go b/ucevcc/ucevcc.go new file mode 100644 index 0000000..c99a887 --- /dev/null +++ b/ucevcc/ucevcc.go @@ -0,0 +1,98 @@ +package ucevcc + +import ( + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + serviceapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +type UCEVCC struct { + service serviceapi.ServiceInterface + + eventCB api.EntityEventCallback + + validEntityTypes []model.EntityTypeType +} + +var _ UCEVCCInterface = (*UCEVCC)(nil) + +func NewUCEVCC(service serviceapi.ServiceInterface, eventCB api.EntityEventCallback) *UCEVCC { + uc := &UCEVCC{ + service: service, + eventCB: eventCB, + } + + uc.validEntityTypes = []model.EntityTypeType{ + model.EntityTypeTypeEV, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (c *UCEVCC) UseCaseName() model.UseCaseNameType { + return model.UseCaseNameTypeEVCommissioningAndConfiguration +} + +func (e *UCEVCC) AddFeatures() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceConfiguration, + model.FeatureTypeTypeIdentification, + model.FeatureTypeTypeDeviceClassification, + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeDeviceDiagnosis, + } + for _, feature := range clientFeatures { + f := localEntity.GetOrAddFeature(feature, model.RoleTypeClient) + f.AddResultCallback(e.HandleResponse) + } +} + +func (e *UCEVCC) AddUseCase() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.AddUseCaseSupport( + model.UseCaseActorTypeCEM, + e.UseCaseName(), + model.SpecificationVersionType("1.0.1"), + "release", + true, + []model.UseCaseScenarioSupportType{1, 2, 3, 4, 5, 6, 7, 8}) +} + +func (e *UCEVCC) UpdateUseCaseAvailability(available bool) { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.SetUseCaseAvailability(model.UseCaseActorTypeCEM, e.UseCaseName(), available) +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *UCEVCC) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return false, api.ErrNoCompatibleEntity + } + + // check if the usecase and mandatory scenarios are supported and + // if the required server features are available + if !entity.Device().VerifyUseCaseScenariosAndFeaturesSupport( + model.UseCaseActorTypeEV, + e.UseCaseName(), + []model.UseCaseScenarioSupportType{1, 2, 3, 8}, + []model.FeatureTypeType{model.FeatureTypeTypeDeviceConfiguration}, + ) { + return false, nil + } + + return true, nil +} diff --git a/ucevcc/ucevcc_test.go b/ucevcc/ucevcc_test.go new file mode 100644 index 0000000..3400cb2 --- /dev/null +++ b/ucevcc/ucevcc_test.go @@ -0,0 +1,67 @@ +package ucevcc + +import ( + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCEVCCSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *UCEVCCSuite) Test_IsUseCaseSupported() { + data, err := s.sut.IsUseCaseSupported(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + data, err = s.sut.IsUseCaseSupported(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: eebusutil.Ptr(model.UseCaseActorTypeEV), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: eebusutil.Ptr(model.UseCaseNameTypeEVCommissioningAndConfiguration), + UseCaseAvailable: eebusutil.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1, 3, 4, 5, 6, 7, 8}, + }, + }, + }, + }, + } + + nodemgmtEntity := s.remoteDevice.Entity([]model.AddressEntityType{0}) + nodeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(nodemgmtEntity, model.FeatureTypeTypeNodeManagement, model.RoleTypeSpecial) + fErr := nodeFeature.UpdateData(model.FunctionTypeNodeManagementUseCaseData, ucData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData = &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: eebusutil.Ptr(model.UseCaseActorTypeEV), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: eebusutil.Ptr(model.UseCaseNameTypeEVCommissioningAndConfiguration), + UseCaseAvailable: eebusutil.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1, 2, 3, 4, 5, 6, 7, 8}, + }, + }, + }, + }, + } + + fErr = nodeFeature.UpdateData(model.FunctionTypeNodeManagementUseCaseData, ucData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/ucevcem/api.go b/ucevcem/api.go new file mode 100644 index 0000000..988412e --- /dev/null +++ b/ucevcem/api.go @@ -0,0 +1,43 @@ +package ucevcem + +import ( + "github.com/enbility/cemd/api" + spineapi "github.com/enbility/spine-go/api" +) + +//go:generate mockery + +// interface for the EV Charging Electricity Measurement UseCase +type UCEVCEMInterface interface { + api.UseCaseInterface + + // return the number of ac connected phases of the EV or 0 if it is unknown + // + // parameters: + // - entity: the entity of the EV + PhasesConnected(entity spineapi.EntityRemoteInterface) (uint, error) + + // Scenario 1 + + // return the last current measurement for each phase of the connected EV + // + // parameters: + // - entity: the entity of the EV + CurrentPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) + + // Scenario 2 + + // return the last power measurement for each phase of the connected EV + // + // parameters: + // - entity: the entity of the EV + PowerPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) + + // Scenario 3 + + // return the charged energy measurement in Wh of the connected EV + // + // parameters: + // - entity: the entity of the EV + EnergyCharged(entity spineapi.EntityRemoteInterface) (float64, error) +} diff --git a/ucevcem/events.go b/ucevcem/events.go new file mode 100644 index 0000000..cd32d22 --- /dev/null +++ b/ucevcem/events.go @@ -0,0 +1,116 @@ +package ucevcem + +import ( + "github.com/enbility/cemd/util" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// handle SPINE events +func (e *UCEVCEM) HandleEvent(payload spineapi.EventPayload) { + // only about events from an EV entity or device changes for this remote device + + if !util.IsCompatibleEntity(payload.Entity, e.validEntityTypes) { + return + } + + if util.IsEntityConnected(payload) { + e.evConnected(payload.Entity) + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + switch payload.Data.(type) { + case *model.ElectricalConnectionDescriptionListDataType: + e.evElectricalConnectionDescriptionDataUpdate(payload) + case *model.MeasurementDescriptionListDataType: + e.evMeasurementDescriptionDataUpdate(payload.Entity) + case *model.MeasurementListDataType: + e.evMeasurementDataUpdate(payload) + } +} + +// an EV was connected +func (e *UCEVCEM) evConnected(entity spineapi.EntityRemoteInterface) { + // initialise features, e.g. subscriptions, descriptions + + if evElectricalConnection, err := util.ElectricalConnection(e.service, entity); err == nil { + if _, err := evElectricalConnection.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + // get electrical connection descriptions + if _, err := evElectricalConnection.RequestDescriptions(); err != nil { + logging.Log().Debug(err) + } + + // get electrical connection parameter descriptions + if _, err := evElectricalConnection.RequestParameterDescriptions(); err != nil { + logging.Log().Debug(err) + } + } + + if evMeasurement, err := util.Measurement(e.service, entity); err == nil { + if _, err := evMeasurement.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + // get measurement descriptions + if _, err := evMeasurement.RequestDescriptions(); err != nil { + logging.Log().Debug(err) + } + + // get measurement constraints + if _, err := evMeasurement.RequestConstraints(); err != nil { + logging.Log().Debug(err) + } + } +} + +// the electrical connection description data of an EV was updated +func (e *UCEVCEM) evElectricalConnectionDescriptionDataUpdate(payload spineapi.EventPayload) { + if payload.Data == nil { + return + } + + data := payload.Data.(*model.ElectricalConnectionDescriptionListDataType) + + for _, item := range data.ElectricalConnectionDescriptionData { + if item.ElectricalConnectionId != nil && item.AcConnectedPhases != nil { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePhasesConnected) + return + } + } +} + +// the measurement description data of an EV was updated +func (e *UCEVCEM) evMeasurementDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if evMeasurement, err := util.Measurement(e.service, entity); err == nil { + // get measurement values + if _, err := evMeasurement.RequestValues(); err != nil { + logging.Log().Debug(err) + } + } +} + +// the measurement data of an EV was updated +func (e *UCEVCEM) evMeasurementDataUpdate(payload spineapi.EventPayload) { + // Scenario 1 + if util.MeasurementCheckPayloadDataForScope(e.service, payload, model.ScopeTypeTypeACCurrent) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateCurrentPerPhase) + } + + // Scenario 2 + if util.MeasurementCheckPayloadDataForScope(e.service, payload, model.ScopeTypeTypeACPower) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePowerPerPhase) + } + + // Scenario 3 + if util.MeasurementCheckPayloadDataForScope(e.service, payload, model.ScopeTypeTypeCharge) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateEnergyCharged) + } +} diff --git a/ucevcem/events_test.go b/ucevcem/events_test.go new file mode 100644 index 0000000..3bfbfd7 --- /dev/null +++ b/ucevcem/events_test.go @@ -0,0 +1,131 @@ +package ucevcem + +import ( + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCEVCEMSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.evEntity + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeEntityChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.Data = eebusutil.Ptr(model.ElectricalConnectionDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.MeasurementDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.MeasurementListDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *UCEVCEMSuite) Test_Failures() { + s.sut.evConnected(s.mockRemoteEntity) + + s.sut.evMeasurementDescriptionDataUpdate(s.mockRemoteEntity) +} + +func (s *UCEVCEMSuite) Test_evElectricalConnectionDescriptionDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evElectricalConnectionDescriptionDataUpdate(payload) + + payload.Entity = s.evEntity + s.sut.evElectricalConnectionDescriptionDataUpdate(payload) + + descData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{}, + } + + payload.Data = descData + + s.sut.evElectricalConnectionDescriptionDataUpdate(payload) + + descData = &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + AcConnectedPhases: eebusutil.Ptr(uint(1)), + }, + }, + } + + payload.Data = descData + + s.sut.evElectricalConnectionDescriptionDataUpdate(payload) +} + +func (s *UCEVCEMSuite) Test_evMeasurementDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evMeasurementDataUpdate(payload) + + payload.Entity = s.evEntity + s.sut.evMeasurementDataUpdate(payload) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACCurrent), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACPower), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeCharge), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evMeasurementDataUpdate(payload) + + data := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(200), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + Value: model.NewScaledNumberType(3000), + }, + }, + } + payload.Data = data + + s.sut.evMeasurementDataUpdate(payload) +} diff --git a/ucevcem/public.go b/ucevcem/public.go new file mode 100644 index 0000000..ed273c3 --- /dev/null +++ b/ucevcem/public.go @@ -0,0 +1,188 @@ +package ucevcem + +import ( + "time" + + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// return the number of ac connected phases of the EV or 0 if it is unknown +func (e *UCEVCEM) PhasesConnected(entity spineapi.EntityRemoteInterface) (uint, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + + evElectricalConnection, err := util.ElectricalConnection(e.service, entity) + if err != nil { + return 0, eebusapi.ErrDataNotAvailable + } + + data, err := evElectricalConnection.GetDescriptions() + if err != nil { + return 0, eebusapi.ErrDataNotAvailable + } + + for _, item := range data { + if item.ElectricalConnectionId != nil && item.AcConnectedPhases != nil { + return *item.AcConnectedPhases, nil + } + } + + // default to 0 if the value is not available + return 0, nil +} + +// return the last current measurement for each phase of the connected EV +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (e *UCEVCEM) CurrentPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return nil, api.ErrNoCompatibleEntity + } + + evMeasurement, err := util.Measurement(e.service, entity) + evElectricalConnection, err2 := util.ElectricalConnection(e.service, entity) + if err != nil || err2 != nil { + return nil, err + } + + measurement := model.MeasurementTypeTypeCurrent + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeACCurrent + data, err := evMeasurement.GetValuesForTypeCommodityScope(measurement, commodity, scope) + if err != nil { + return nil, err + } + + var result []float64 + refetch := true + compare := time.Now().Add(-1 * time.Minute) + + for _, phase := range util.PhaseNameMapping { + for _, item := range data { + if item.Value == nil { + continue + } + + elParam, err := evElectricalConnection.GetParameterDescriptionForMeasurementId(*item.MeasurementId) + if err != nil || elParam.AcMeasuredPhases == nil || *elParam.AcMeasuredPhases != phase { + continue + } + + phaseValue := item.Value.GetValue() + result = append(result, phaseValue) + + if item.Timestamp != nil { + if timestamp, err := item.Timestamp.GetTime(); err == nil { + refetch = timestamp.Before(compare) + } + } + } + } + + // if there was no timestamp provided or the time for the last value + // is older than 1 minute, send a read request + if refetch { + _, _ = evMeasurement.RequestValues() + } + + return result, nil +} + +// return the last power measurement for each phase of the connected EV +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (e *UCEVCEM) PowerPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return nil, api.ErrNoCompatibleEntity + } + + evMeasurement, err := util.Measurement(e.service, entity) + evElectricalConnection, err2 := util.ElectricalConnection(e.service, entity) + if err != nil || err2 != nil { + return nil, err + } + + var data []model.MeasurementDataType + + powerAvailable := true + measurement := model.MeasurementTypeTypePower + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeACPower + data, err = 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 = evMeasurement.GetValuesForTypeCommodityScope(measurement, commodity, scope) + if err != nil { + return nil, err + } + } + + var result []float64 + + for _, phase := range util.PhaseNameMapping { + for _, item := range data { + if item.Value == nil { + continue + } + + elParam, err := 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) + } + } + + return result, nil +} + +// return the charged energy measurement in Wh of the connected EV +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (e *UCEVCEM) EnergyCharged(entity spineapi.EntityRemoteInterface) (float64, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + + evMeasurement, err := util.Measurement(e.service, entity) + if err != nil { + return 0, err + } + + measurement := model.MeasurementTypeTypeEnergy + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeCharge + data, err := 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, eebusapi.ErrDataNotAvailable + } + + return value.GetValue(), err +} diff --git a/ucevcem/public_test.go b/ucevcem/public_test.go new file mode 100644 index 0000000..4bab63f --- /dev/null +++ b/ucevcem/public_test.go @@ -0,0 +1,289 @@ +package ucevcem + +import ( + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCEVCEMSuite) Test_EVConnectedPhases() { + data, err := s.sut.PhasesConnected(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), uint(0), data) + + data, err = s.sut.PhasesConnected(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), uint(0), data) + + descData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PhasesConnected(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), uint(0), data) + + descData = &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + AcConnectedPhases: eebusutil.Ptr(uint(1)), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeElectricalConnectionDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PhasesConnected(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), uint(1), data) +} + +func (s *UCEVCEMSuite) Test_EVCurrentPerPhase() { + data, err := s.sut.CurrentPerPhase(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + data, err = s.sut.CurrentPerPhase(s.evEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + paramDesc := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACCurrent), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramDesc, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerPerPhase(s.evEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + measDesc := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypeCurrent), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACCurrent), + }, + }, + } + + rFeature = s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, measDesc, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.CurrentPerPhase(s.evEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.CurrentPerPhase(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data[0]) +} + +func (s *UCEVCEMSuite) Test_EVPowerPerPhase_Power() { + data, err := s.sut.PowerPerPhase(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + data, err = s.sut.PowerPerPhase(s.evEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + paramDesc := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACPower), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramDesc, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerPerPhase(s.evEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + measDesc := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypePower), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACPower), + }, + }, + } + + rFeature = s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, measDesc, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerPerPhase(s.evEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(80), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerPerPhase(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 80.0, data[0]) +} + +func (s *UCEVCEMSuite) Test_EVPowerPerPhase_Current() { + data, err := s.sut.PowerPerPhase(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + data, err = s.sut.PowerPerPhase(s.evEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + paramDesc := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACCurrent), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramDesc, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerPerPhase(s.evEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + measDesc := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypeCurrent), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACCurrent), + }, + }, + } + + rFeature = s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, measDesc, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerPerPhase(s.evEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerPerPhase(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 2300.0, data[0]) +} + +func (s *UCEVCEMSuite) Test_EVChargedEnergy() { + data, err := s.sut.EnergyCharged(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.EnergyCharged(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measDesc := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeCharge), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, measDesc, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyCharged(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(80), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyCharged(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 80.0, data) +} diff --git a/ucevcem/testhelper_test.go b/ucevcem/testhelper_test.go new file mode 100644 index 0000000..6478268 --- /dev/null +++ b/ucevcem/testhelper_test.go @@ -0,0 +1,178 @@ +package ucevcem + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/cemd/api" + eebusapi "github.com/enbility/eebus-go/api" + eebusmocks "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestEVCEMSuite(t *testing.T) { + suite.Run(t, new(UCEVCEMSuite)) +} + +type UCEVCEMSuite struct { + suite.Suite + + sut *UCEVCEM + + service eebusapi.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *mocks.EntityRemoteInterface + evEntity spineapi.EntityRemoteInterface +} + +func (s *UCEVCEMSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { +} + +func (s *UCEVCEMSuite) BeforeTest(suiteName, testName string) { + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := eebusapi.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := eebusmocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := mocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = mocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := mocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + s.sut = NewUCEVCEM(s.service, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + var entities []spineapi.EntityRemoteInterface + s.remoteDevice, entities = setupDevices(s.service, s.T()) + s.evEntity = entities[1] +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService eebusapi.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + []spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeElectricalConnection, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionDescriptionListData, + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + model.FunctionTypeElectricalConnectionPermittedValueSetListData, + }, + }, + { + model.FeatureTypeTypeMeasurement, + []model.FunctionType{ + model.FunctionTypeMeasurementDescriptionListData, + model.FunctionTypeMeasurementListData, + }, + }, + } + + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: eebusutil.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + Feature: eebusutil.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: eebusutil.Ptr(feature.featureType), + Role: eebusutil.Ptr(model.RoleTypeServer), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeEVSE), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeEV), + }, + }, + }, + FeatureInformation: featureInformations, + } + + entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + remoteDevice.UpdateDevice(detailedData.DeviceInformation.Description) + + localDevice.AddRemoteDeviceForSki(remoteSki, remoteDevice) + + return remoteDevice, entities +} diff --git a/ucevcem/types.go b/ucevcem/types.go new file mode 100644 index 0000000..e834202 --- /dev/null +++ b/ucevcem/types.go @@ -0,0 +1,33 @@ +package ucevcem + +import "github.com/enbility/cemd/api" + +const ( + // EV number of connected phases data updated + // + // Use `PhasesConnected` to get the current data + // + // Use Case EVCEM, Scenario 1 + DataUpdatePhasesConnected api.EventType = "DataUpdatePhasesConnected" + + // EV current measurement data updated + // + // Use `CurrentPerPhase` to get the current data + // + // Use Case EVCEM, Scenario 1 + DataUpdateCurrentPerPhase api.EventType = "DataUpdateCurrentPerPhase" + + // EV power measurement data updated + // + // Use `PowerPerPhase` to get the current data + // + // Use Case EVCEM, Scenario 2 + DataUpdatePowerPerPhase api.EventType = "DataUpdatePowerPerPhase" + + // EV charging energy measurement data updated + // + // Use `EnergyCharged` to get the current data + // + // Use Case EVCEM, Scenario 3 + DataUpdateEnergyCharged api.EventType = "DataUpdateEnergyCharged" +) diff --git a/ucevcem/ucevcem.go b/ucevcem/ucevcem.go new file mode 100644 index 0000000..935372a --- /dev/null +++ b/ucevcem/ucevcem.go @@ -0,0 +1,94 @@ +package ucevcem + +import ( + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + serviceapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +type UCEVCEM struct { + service serviceapi.ServiceInterface + + eventCB api.EntityEventCallback + + validEntityTypes []model.EntityTypeType +} + +var _ UCEVCEMInterface = (*UCEVCEM)(nil) + +func NewUCEVCEM(service serviceapi.ServiceInterface, eventCB api.EntityEventCallback) *UCEVCEM { + uc := &UCEVCEM{ + service: service, + eventCB: eventCB, + } + + uc.validEntityTypes = []model.EntityTypeType{ + model.EntityTypeTypeEV, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (c *UCEVCEM) UseCaseName() model.UseCaseNameType { + return model.UseCaseNameTypeMeasurementOfElectricityDuringEVCharging +} + +func (e *UCEVCEM) AddFeatures() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + } + for _, feature := range clientFeatures { + _ = localEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } +} + +func (e *UCEVCEM) AddUseCase() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.AddUseCaseSupport( + model.UseCaseActorTypeCEM, + e.UseCaseName(), + model.SpecificationVersionType("1.0.1"), + "release", + true, + []model.UseCaseScenarioSupportType{1, 2, 3}) +} + +func (e *UCEVCEM) UpdateUseCaseAvailability(available bool) { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.SetUseCaseAvailability(model.UseCaseActorTypeCEM, e.UseCaseName(), available) +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *UCEVCEM) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return false, api.ErrNoCompatibleEntity + } + + // check if the usecase and mandatory scenarios are supported and + // if the required server features are available + if !entity.Device().VerifyUseCaseScenariosAndFeaturesSupport( + model.UseCaseActorTypeEV, + e.UseCaseName(), + nil, + nil, + ) { + return false, nil + } + + return true, nil +} diff --git a/ucevcem/ucevcem_test.go b/ucevcem/ucevcem_test.go new file mode 100644 index 0000000..978ae3d --- /dev/null +++ b/ucevcem/ucevcem_test.go @@ -0,0 +1,45 @@ +package ucevcem + +import ( + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCEVCEMSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *UCEVCEMSuite) Test_IsUseCaseSupported() { + data, err := s.sut.IsUseCaseSupported(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + data, err = s.sut.IsUseCaseSupported(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: eebusutil.Ptr(model.UseCaseActorTypeEV), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: eebusutil.Ptr(model.UseCaseNameTypeMeasurementOfElectricityDuringEVCharging), + UseCaseAvailable: eebusutil.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1}, + }, + }, + }, + }, + } + + nodemgmtEntity := s.remoteDevice.Entity([]model.AddressEntityType{0}) + nodeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(nodemgmtEntity, model.FeatureTypeTypeNodeManagement, model.RoleTypeSpecial) + fErr := nodeFeature.UpdateData(model.FunctionTypeNodeManagementUseCaseData, ucData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/ucevsecc/api.go b/ucevsecc/api.go new file mode 100644 index 0000000..675274c --- /dev/null +++ b/ucevsecc/api.go @@ -0,0 +1,30 @@ +package ucevsecc + +import ( + "github.com/enbility/cemd/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +//go:generate mockery + +// interface for the EVSE Commissioning and Configuration UseCase +type UCEVSECCInterface interface { + api.UseCaseInterface + + // the manufacturer data of an EVSE + // + // parameters: + // - entity: the entity of the EV + // + // returns deviceName, serialNumber, error + ManufacturerData(entity spineapi.EntityRemoteInterface) (api.ManufacturerData, error) + + // the operating state data of an EVSE + // + // parameters: + // - entity: the entity of the EV + // + // returns operatingState, lastErrorCode, error + OperatingState(entity spineapi.EntityRemoteInterface) (model.DeviceDiagnosisOperatingStateType, string, error) +} diff --git a/ucevsecc/events.go b/ucevsecc/events.go new file mode 100644 index 0000000..3892297 --- /dev/null +++ b/ucevsecc/events.go @@ -0,0 +1,78 @@ +package ucevsecc + +import ( + "github.com/enbility/cemd/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// handle SPINE events +func (e *UCEVSECC) HandleEvent(payload spineapi.EventPayload) { + // only about events from an EVSE entity or device changes for this remote device + + if !util.IsCompatibleEntity(payload.Entity, e.validEntityTypes) { + return + } + + if util.IsEntityConnected(payload) { + e.evseConnected(payload) + return + } else if util.IsEntityDisconnected(payload) { + e.evseDisconnected(payload) + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + + switch payload.Data.(type) { + case *model.DeviceClassificationManufacturerDataType: + e.evseManufacturerDataUpdate(payload) + case *model.DeviceDiagnosisStateDataType: + e.evseStateUpdate(payload) + } +} + +// an EVSE was connected +func (e *UCEVSECC) evseConnected(payload spineapi.EventPayload) { + if evseDeviceClassification, err := util.DeviceClassification(e.service, payload.Entity); err == nil { + _, _ = evseDeviceClassification.RequestManufacturerDetails() + } + + if evseDeviceDiagnosis, err := util.DeviceDiagnosis(e.service, payload.Entity); err == nil { + _, _ = evseDeviceDiagnosis.RequestState() + } + + e.eventCB(payload.Ski, payload.Device, payload.Entity, EvseConnected) +} + +// an EVSE was disconnected +func (e *UCEVSECC) evseDisconnected(payload spineapi.EventPayload) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, EvseDisconnected) +} + +// the manufacturer Data of an EVSE was updated +func (e *UCEVSECC) evseManufacturerDataUpdate(payload spineapi.EventPayload) { + evDeviceClassification, err := util.DeviceClassification(e.service, payload.Entity) + if err != nil { + return + } + + if _, err := evDeviceClassification.GetManufacturerDetails(); err == nil { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateManufacturerData) + } +} + +// the operating State of an EVSE was updated +func (e *UCEVSECC) evseStateUpdate(payload spineapi.EventPayload) { + evDeviceDiagnosis, err := util.DeviceDiagnosis(e.service, payload.Entity) + if err != nil { + return + } + + if _, err := evDeviceDiagnosis.GetState(); err == nil { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOperatingState) + } +} diff --git a/ucevsecc/events_test.go b/ucevsecc/events_test.go new file mode 100644 index 0000000..3db4437 --- /dev/null +++ b/ucevsecc/events_test.go @@ -0,0 +1,86 @@ +package ucevsecc + +import ( + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCEVSECCSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.evseEntity + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDeviceChange + payload.ChangeType = spineapi.ElementChangeRemove + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeEntityChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeEntityChange + payload.ChangeType = spineapi.ElementChangeRemove + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.Data = eebusutil.Ptr(model.DeviceClassificationManufacturerDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.DeviceDiagnosisStateDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *UCEVSECCSuite) Test_evseManufacturerDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evseManufacturerDataUpdate(payload) + + payload.Entity = s.evseEntity + s.sut.evseManufacturerDataUpdate(payload) + + data := &model.DeviceClassificationManufacturerDataType{ + BrandName: eebusutil.Ptr(model.DeviceClassificationStringType("test")), + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evseEntity, model.FeatureTypeTypeDeviceClassification, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceClassificationManufacturerData, data, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evseManufacturerDataUpdate(payload) +} + +func (s *UCEVSECCSuite) Test_evseStateUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evseStateUpdate(payload) + + payload.Entity = s.evseEntity + s.sut.evseStateUpdate(payload) + + data := &model.DeviceDiagnosisStateDataType{ + OperatingState: eebusutil.Ptr(model.DeviceDiagnosisOperatingStateTypeNormalOperation), + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evseEntity, model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceDiagnosisStateData, data, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evseStateUpdate(payload) +} diff --git a/ucevsecc/public.go b/ucevsecc/public.go new file mode 100644 index 0000000..0f48139 --- /dev/null +++ b/ucevsecc/public.go @@ -0,0 +1,53 @@ +package ucevsecc + +import ( + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// the manufacturer data of an EVSE +// returns deviceName, serialNumber, error +func (e *UCEVSECC) ManufacturerData( + entity spineapi.EntityRemoteInterface, +) ( + api.ManufacturerData, + error, +) { + return util.ManufacturerData(e.service, entity, e.validEntityTypes) +} + +// the operating state data of an EVSE +// returns operatingState, lastErrorCode, error +func (e *UCEVSECC) OperatingState( + entity spineapi.EntityRemoteInterface, +) ( + model.DeviceDiagnosisOperatingStateType, string, error, +) { + operatingState := model.DeviceDiagnosisOperatingStateTypeNormalOperation + lastErrorCode := "" + + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return operatingState, lastErrorCode, api.ErrNoCompatibleEntity + } + + evseDeviceDiagnosis, err := util.DeviceDiagnosis(e.service, entity) + if err != nil { + return operatingState, lastErrorCode, err + } + + data, err := evseDeviceDiagnosis.GetState() + if err != nil { + return operatingState, lastErrorCode, err + } + + if data.OperatingState != nil { + operatingState = *data.OperatingState + } + if data.LastErrorCode != nil { + lastErrorCode = string(*data.LastErrorCode) + } + + return operatingState, lastErrorCode, nil +} diff --git a/ucevsecc/public_test.go b/ucevsecc/public_test.go new file mode 100644 index 0000000..2a884b0 --- /dev/null +++ b/ucevsecc/public_test.go @@ -0,0 +1,86 @@ +package ucevsecc + +import ( + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCEVSECCSuite) Test_EVSEManufacturerData() { + _, err := s.sut.ManufacturerData(nil) + assert.NotNil(s.T(), err) + + _, err = s.sut.ManufacturerData(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.ManufacturerData(s.evseEntity) + assert.NotNil(s.T(), err) + + descData := &model.DeviceClassificationManufacturerDataType{} + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evseEntity, model.FeatureTypeTypeDeviceClassification, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceClassificationManufacturerData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err := s.sut.ManufacturerData(s.evseEntity) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), data) + assert.Equal(s.T(), "", data.DeviceName) + assert.Equal(s.T(), "", data.SerialNumber) + + descData = &model.DeviceClassificationManufacturerDataType{ + DeviceName: eebusutil.Ptr(model.DeviceClassificationStringType("test")), + SerialNumber: eebusutil.Ptr(model.DeviceClassificationStringType("12345")), + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceClassificationManufacturerData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.ManufacturerData(s.evseEntity) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), data) + assert.Equal(s.T(), "test", data.DeviceName) + assert.Equal(s.T(), "12345", data.SerialNumber) + assert.Equal(s.T(), "", data.BrandName) +} + +func (s *UCEVSECCSuite) Test_EVSEOperatingState() { + data, errCode, err := s.sut.OperatingState(nil) + assert.Equal(s.T(), model.DeviceDiagnosisOperatingStateTypeNormalOperation, data) + assert.Equal(s.T(), "", errCode) + assert.Nil(s.T(), nil, err) + + data, errCode, err = s.sut.OperatingState(s.mockRemoteEntity) + assert.Equal(s.T(), model.DeviceDiagnosisOperatingStateTypeNormalOperation, data) + assert.Equal(s.T(), "", errCode) + assert.NotNil(s.T(), err) + + data, errCode, err = s.sut.OperatingState(s.evseEntity) + assert.Equal(s.T(), model.DeviceDiagnosisOperatingStateTypeNormalOperation, data) + assert.Equal(s.T(), "", errCode) + assert.NotNil(s.T(), err) + + descData := &model.DeviceDiagnosisStateDataType{} + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evseEntity, model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceDiagnosisStateData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, errCode, err = s.sut.OperatingState(s.evseEntity) + assert.Equal(s.T(), model.DeviceDiagnosisOperatingStateTypeNormalOperation, data) + assert.Equal(s.T(), "", errCode) + assert.Nil(s.T(), err) + + descData = &model.DeviceDiagnosisStateDataType{ + OperatingState: eebusutil.Ptr(model.DeviceDiagnosisOperatingStateTypeStandby), + LastErrorCode: eebusutil.Ptr(model.LastErrorCodeType("error")), + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceDiagnosisStateData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, errCode, err = s.sut.OperatingState(s.evseEntity) + assert.Equal(s.T(), model.DeviceDiagnosisOperatingStateTypeStandby, data) + assert.Equal(s.T(), "error", errCode) + assert.Nil(s.T(), err) +} diff --git a/ucevsecc/testhelper_test.go b/ucevsecc/testhelper_test.go new file mode 100644 index 0000000..f835343 --- /dev/null +++ b/ucevsecc/testhelper_test.go @@ -0,0 +1,177 @@ +package ucevsecc + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/cemd/api" + eebusapi "github.com/enbility/eebus-go/api" + eebusmocks "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestEVSECCSuite(t *testing.T) { + suite.Run(t, new(UCEVSECCSuite)) +} + +type UCEVSECCSuite struct { + suite.Suite + + sut *UCEVSECC + + service eebusapi.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *mocks.EntityRemoteInterface + evseEntity spineapi.EntityRemoteInterface +} + +func (s *UCEVSECCSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { +} + +func (s *UCEVSECCSuite) BeforeTest(suiteName, testName string) { + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := eebusapi.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := eebusmocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := mocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = mocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := mocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + s.sut = NewUCEVSECC(s.service, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + var entities []spineapi.EntityRemoteInterface + s.remoteDevice, entities = setupDevices(s.service, s.T()) + s.evseEntity = entities[0] +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService eebusapi.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + []spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeDeviceClassification, + []model.FunctionType{ + model.FunctionTypeDeviceClassificationManufacturerData, + }, + }, + {model.FeatureTypeTypeDeviceDiagnosis, + []model.FunctionType{ + model.FunctionTypeDeviceDiagnosisStateData, + }, + }, + } + + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: eebusutil.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + Feature: eebusutil.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: eebusutil.Ptr(feature.featureType), + Role: eebusutil.Ptr(model.RoleTypeServer), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeEVSE), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeEV), + }, + }, + }, + FeatureInformation: featureInformations, + } + + entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + remoteDevice.UpdateDevice(detailedData.DeviceInformation.Description) + + localDevice.AddRemoteDeviceForSki(remoteSki, remoteDevice) + + return remoteDevice, entities +} diff --git a/ucevsecc/types.go b/ucevsecc/types.go new file mode 100644 index 0000000..7a463f8 --- /dev/null +++ b/ucevsecc/types.go @@ -0,0 +1,29 @@ +package ucevsecc + +import "github.com/enbility/cemd/api" + +const ( + // An EVSE was connected + EvseConnected api.EventType = "EvseConnected" + + // An EVSE was disconnected + EvseDisconnected api.EventType = "EvseDisconnected" + + // EVSE manufacturer data was updated + // + // Use `ManufacturerData` to get the current data + // + // Use Case EVSECC, Scenario 1 + // + // The entity of the message is the entity of the EVSE + DataUpdateManufacturerData api.EventType = "DataUpdateManufacturerData" + + // EVSE operation state was updated + // + // Use `OperatingState` to get the current data + // + // Use Case EVSECC, Scenario 2 + // + // The entity of the message is the entity of the EVSE + DataUpdateOperatingState api.EventType = "DataUpdateOperatingState" +) diff --git a/ucevsecc/ucevsecc.go b/ucevsecc/ucevsecc.go new file mode 100644 index 0000000..ddf6c0f --- /dev/null +++ b/ucevsecc/ucevsecc.go @@ -0,0 +1,104 @@ +package ucevsecc + +import ( + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +type UCEVSECC struct { + service eebusapi.ServiceInterface + + eventCB api.EntityEventCallback + + validEntityTypes []model.EntityTypeType +} + +var _ UCEVSECCInterface = (*UCEVSECC)(nil) + +func NewUCEVSECC(service eebusapi.ServiceInterface, eventCB api.EntityEventCallback) *UCEVSECC { + uc := &UCEVSECC{ + service: service, + eventCB: eventCB, + } + + uc.validEntityTypes = []model.EntityTypeType{ + model.EntityTypeTypeEVSE, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (c *UCEVSECC) UseCaseName() model.UseCaseNameType { + return model.UseCaseNameTypeEVSECommissioningAndConfiguration +} + +func (e *UCEVSECC) AddFeatures() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceClassification, + model.FeatureTypeTypeDeviceDiagnosis, + } + + for _, feature := range clientFeatures { + _ = localEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } +} + +func (e *UCEVSECC) AddUseCase() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.AddUseCaseSupport( + model.UseCaseActorTypeCEM, + e.UseCaseName(), + model.SpecificationVersionType("1.0.1"), + "release", + true, + []model.UseCaseScenarioSupportType{1, 2}) +} + +func (e *UCEVSECC) UpdateUseCaseAvailability(available bool) { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.SetUseCaseAvailability(model.UseCaseActorTypeCEM, e.UseCaseName(), available) +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *UCEVSECC) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return false, api.ErrNoCompatibleEntity + } + + // check if the usecase and mandatory scenarios are supported and + // if the required server features are available + if !entity.Device().VerifyUseCaseScenariosAndFeaturesSupport( + model.UseCaseActorTypeEVSE, + e.UseCaseName(), + []model.UseCaseScenarioSupportType{2}, + []model.FeatureTypeType{model.FeatureTypeTypeDeviceDiagnosis}, + ) { + // Workaround for the Porsche Mobile Charger Connect that falsely reports + // the usecase to be on the EV actor + if !entity.Device().VerifyUseCaseScenariosAndFeaturesSupport( + model.UseCaseActorTypeEV, + e.UseCaseName(), + []model.UseCaseScenarioSupportType{2}, + []model.FeatureTypeType{model.FeatureTypeTypeDeviceDiagnosis}, + ) { + return false, nil + } + } + + return true, nil +} diff --git a/ucevsecc/ucevsecc_test.go b/ucevsecc/ucevsecc_test.go new file mode 100644 index 0000000..54b7b8c --- /dev/null +++ b/ucevsecc/ucevsecc_test.go @@ -0,0 +1,45 @@ +package ucevsecc + +import ( + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCEVSECCSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *UCEVSECCSuite) Test_IsUseCaseSupported() { + data, err := s.sut.IsUseCaseSupported(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + data, err = s.sut.IsUseCaseSupported(s.evseEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: eebusutil.Ptr(model.UseCaseActorTypeEV), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: eebusutil.Ptr(model.UseCaseNameTypeEVSECommissioningAndConfiguration), + UseCaseAvailable: eebusutil.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{2}, + }, + }, + }, + }, + } + + nodemgmtEntity := s.remoteDevice.Entity([]model.AddressEntityType{0}) + nodeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(nodemgmtEntity, model.FeatureTypeTypeNodeManagement, model.RoleTypeSpecial) + fErr := nodeFeature.UpdateData(model.FunctionTypeNodeManagementUseCaseData, ucData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.evseEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/ucevsoc/api.go b/ucevsoc/api.go new file mode 100644 index 0000000..feb71bd --- /dev/null +++ b/ucevsoc/api.go @@ -0,0 +1,23 @@ +package ucevsoc + +import ( + "github.com/enbility/cemd/api" + spineapi "github.com/enbility/spine-go/api" +) + +//go:generate mockery + +// interface for the EV State Of Charge UseCase +type UCEVSOCInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // return the EVscurrent state of charge of the EV or an error it is unknown + // + // parameters: + // - entity: the entity of the EV + StateOfCharge(entity spineapi.EntityRemoteInterface) (float64, error) + + // Scenario 2 to 4 are not supported, as there is no EV supporting this as of today +} diff --git a/ucevsoc/events.go b/ucevsoc/events.go new file mode 100644 index 0000000..5b46c27 --- /dev/null +++ b/ucevsoc/events.go @@ -0,0 +1,62 @@ +package ucevsoc + +import ( + "github.com/enbility/cemd/util" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// handle SPINE events +func (e *UCEVSOC) HandleEvent(payload spineapi.EventPayload) { + // only about events from an EV entity or device changes for this remote device + + if !util.IsCompatibleEntity(payload.Entity, e.validEntityTypes) { + return + } + + if util.IsEntityConnected(payload) { + e.evConnected(payload.Entity) + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + + // the codefactor warning is invalid, as .(type) check can not be replaced with if then + //revive:disable-next-line + switch payload.Data.(type) { + case *model.MeasurementListDataType: + e.evMeasurementDataUpdate(payload) + } +} + +// an EV was connected +func (e *UCEVSOC) evConnected(entity spineapi.EntityRemoteInterface) { + // initialise features, e.g. subscriptions, descriptions + if evMeasurement, err := util.Measurement(e.service, entity); err == nil { + if _, err := evMeasurement.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + // get measurement descriptions + if _, err := evMeasurement.RequestDescriptions(); err != nil { + logging.Log().Debug(err) + } + + // get measurement constraints + if _, err := evMeasurement.RequestConstraints(); err != nil { + logging.Log().Debug(err) + } + } +} + +// the measurement data of an EV was updated +func (e *UCEVSOC) evMeasurementDataUpdate(payload spineapi.EventPayload) { + // Scenario 1 + if util.MeasurementCheckPayloadDataForScope(e.service, payload, model.ScopeTypeTypeStateOfCharge) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateStateOfCharge) + } +} diff --git a/ucevsoc/events_test.go b/ucevsoc/events_test.go new file mode 100644 index 0000000..bc8d74a --- /dev/null +++ b/ucevsoc/events_test.go @@ -0,0 +1,91 @@ +package ucevsoc + +import ( + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCEVSOCSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.evEntity + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeEntityChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.Data = eebusutil.Ptr(model.MeasurementListDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *UCEVSOCSuite) Test_Failures() { + s.sut.evConnected(s.mockRemoteEntity) +} + +func (s *UCEVSOCSuite) Test_evMeasurementDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evMeasurementDataUpdate(payload) + + payload.Entity = s.evEntity + s.sut.evMeasurementDataUpdate(payload) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeStateOfCharge), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeStateOfHealth), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeTravelRange), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evMeasurementDataUpdate(payload) + + data := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(200), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + Value: model.NewScaledNumberType(3000), + }, + }, + } + + payload.Data = data + + s.sut.evMeasurementDataUpdate(payload) +} diff --git a/ucevsoc/public.go b/ucevsoc/public.go new file mode 100644 index 0000000..2b5acb8 --- /dev/null +++ b/ucevsoc/public.go @@ -0,0 +1,41 @@ +package ucevsoc + +import ( + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// return the last known SoC of the connected EV +// +// only works with a current ISO15118-2 with VAS or ISO15118-20 +// communication between EVSE and EV +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (e *UCEVSOC) StateOfCharge(entity spineapi.EntityRemoteInterface) (float64, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + + evMeasurement, err := util.Measurement(e.service, entity) + if err != nil || evMeasurement == nil { + return 0, err + } + + data, err := evMeasurement.GetValuesForTypeCommodityScope(model.MeasurementTypeTypePercentage, model.CommodityTypeTypeElectricity, model.ScopeTypeTypeStateOfCharge) + if err != nil { + return 0, err + } + + // we assume there is only one value, nil is already checked + value := data[0].Value + if value == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + return value.GetValue(), nil +} diff --git a/ucevsoc/public_test.go b/ucevsoc/public_test.go new file mode 100644 index 0000000..ce89c2f --- /dev/null +++ b/ucevsoc/public_test.go @@ -0,0 +1,91 @@ +package ucevsoc + +import ( + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCEVSOCSuite) Test_StateOfCharge() { + data, err := s.sut.StateOfCharge(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.StateOfCharge(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: eebusutil.Ptr(model.UseCaseActorTypeEV), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: eebusutil.Ptr(model.UseCaseNameTypeEVStateOfCharge), + UseCaseAvailable: eebusutil.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1}, + }, + }, + }, + }, + } + + nodemgmtEntity := s.remoteDevice.Entity([]model.AddressEntityType{0}) + nodeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(nodemgmtEntity, model.FeatureTypeTypeNodeManagement, model.RoleTypeSpecial) + fErr := nodeFeature.UpdateData(model.FunctionTypeNodeManagementUseCaseData, ucData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.StateOfCharge(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measDesc := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypePercentage), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeStateOfCharge), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, measDesc, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.StateOfCharge(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.StateOfCharge(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData = &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(80), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.StateOfCharge(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 80.0, data) +} diff --git a/ucevsoc/testhelper_test.go b/ucevsoc/testhelper_test.go new file mode 100644 index 0000000..9c14bf2 --- /dev/null +++ b/ucevsoc/testhelper_test.go @@ -0,0 +1,178 @@ +package ucevsoc + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/cemd/api" + eebusapi "github.com/enbility/eebus-go/api" + eebusmocks "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestEVSOCSuite(t *testing.T) { + suite.Run(t, new(UCEVSOCSuite)) +} + +type UCEVSOCSuite struct { + suite.Suite + + sut *UCEVSOC + + service eebusapi.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *mocks.EntityRemoteInterface + evEntity spineapi.EntityRemoteInterface +} + +func (s *UCEVSOCSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { +} + +func (s *UCEVSOCSuite) BeforeTest(suiteName, testName string) { + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := eebusapi.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := eebusmocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := mocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = mocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := mocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + s.sut = NewUCEVSOC(s.service, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + var entities []spineapi.EntityRemoteInterface + s.remoteDevice, entities = setupDevices(s.service, s.T()) + s.evEntity = entities[1] +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService eebusapi.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + []spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeMeasurement, + []model.FunctionType{ + model.FunctionTypeMeasurementDescriptionListData, + model.FunctionTypeMeasurementListData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + }, + }, + } + + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: eebusutil.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + Feature: eebusutil.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: eebusutil.Ptr(feature.featureType), + Role: eebusutil.Ptr(model.RoleTypeServer), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeEVSE), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeEV), + }, + }, + }, + FeatureInformation: featureInformations, + } + + entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + remoteDevice.UpdateDevice(detailedData.DeviceInformation.Description) + + localDevice.AddRemoteDeviceForSki(remoteSki, remoteDevice) + + return remoteDevice, entities +} diff --git a/ucevsoc/types.go b/ucevsoc/types.go new file mode 100644 index 0000000..b5f2b0a --- /dev/null +++ b/ucevsoc/types.go @@ -0,0 +1,12 @@ +package ucevsoc + +import "github.com/enbility/cemd/api" + +const ( + // EV state of charge data was updated + // + // Use `StateOfCharge` to get the current data + // + // Use Case EVSOC, Scenario 1 + DataUpdateStateOfCharge api.EventType = "DataUpdateStateOfCharge" +) diff --git a/ucevsoc/ucevsoc.go b/ucevsoc/ucevsoc.go new file mode 100644 index 0000000..46b80a2 --- /dev/null +++ b/ucevsoc/ucevsoc.go @@ -0,0 +1,105 @@ +package ucevsoc + +import ( + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +type UCEVSOC struct { + service eebusapi.ServiceInterface + + eventCB api.EntityEventCallback + + validEntityTypes []model.EntityTypeType +} + +var _ UCEVSOCInterface = (*UCEVSOC)(nil) + +func NewUCEVSOC(service eebusapi.ServiceInterface, eventCB api.EntityEventCallback) *UCEVSOC { + uc := &UCEVSOC{ + service: service, + eventCB: eventCB, + } + + uc.validEntityTypes = []model.EntityTypeType{ + model.EntityTypeTypeEV, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (c *UCEVSOC) UseCaseName() model.UseCaseNameType { + return model.UseCaseNameTypeEVStateOfCharge +} + +func (e *UCEVSOC) AddFeatures() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + } + for _, feature := range clientFeatures { + _ = localEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } +} + +func (e *UCEVSOC) AddUseCase() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.AddUseCaseSupport( + model.UseCaseActorTypeCEM, + e.UseCaseName(), + model.SpecificationVersionType("1.0.0"), + "RC1", + true, + []model.UseCaseScenarioSupportType{1}) +} + +func (e *UCEVSOC) UpdateUseCaseAvailability(available bool) { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.SetUseCaseAvailability(model.UseCaseActorTypeCEM, e.UseCaseName(), available) +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *UCEVSOC) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return false, api.ErrNoCompatibleEntity + } + + // check if the usecase and mandatory scenarios are supported and + // if the required server features are available + if !entity.Device().VerifyUseCaseScenariosAndFeaturesSupport( + model.UseCaseActorTypeEV, + e.UseCaseName(), + []model.UseCaseScenarioSupportType{1}, + []model.FeatureTypeType{model.FeatureTypeTypeMeasurement}, + ) { + return false, nil + } + + // check for required features + evMeasurement, err := util.Measurement(e.service, entity) + if err != nil || evMeasurement == nil { + return false, eebusapi.ErrFunctionNotSupported + } + + // check if measurement description contains an element with scope SOC + if _, err = evMeasurement.GetDescriptionsForScope(model.ScopeTypeTypeStateOfCharge); err != nil { + return false, err + } + + return true, nil +} diff --git a/ucevsoc/ucevsoc_test.go b/ucevsoc/ucevsoc_test.go new file mode 100644 index 0000000..78b8608 --- /dev/null +++ b/ucevsoc/ucevsoc_test.go @@ -0,0 +1,62 @@ +package ucevsoc + +import ( + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCEVSOCSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *UCEVSOCSuite) Test_IsUseCaseSupported() { + data, err := s.sut.IsUseCaseSupported(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + data, err = s.sut.IsUseCaseSupported(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: eebusutil.Ptr(model.UseCaseActorTypeEV), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: eebusutil.Ptr(model.UseCaseNameTypeEVStateOfCharge), + UseCaseAvailable: eebusutil.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1}, + }, + }, + }, + }, + } + + nodemgmtEntity := s.remoteDevice.Entity([]model.AddressEntityType{0}) + nodeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(nodemgmtEntity, model.FeatureTypeTypeNodeManagement, model.RoleTypeSpecial) + fErr := nodeFeature.UpdateData(model.FunctionTypeNodeManagementUseCaseData, ucData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeStateOfCharge), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/uclpc/api.go b/uclpc/api.go new file mode 100644 index 0000000..ceeb0f0 --- /dev/null +++ b/uclpc/api.go @@ -0,0 +1,87 @@ +package uclpc + +import ( + "time" + + "github.com/enbility/cemd/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +//go:generate mockery + +// interface for the Limitation of Power Consumption UseCase +type UCLPCInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // return the current consumption limit data + // + // parameters: + // - entity: the entity of the e.g. EVSE + // + // return values: + // - limit: load limit data + // + // possible errors: + // - ErrDataNotAvailable if no such limit is (yet) available + // - and others + ConsumptionLimit(entity spineapi.EntityRemoteInterface) (limit api.LoadLimit, resultErr error) + + // send new LoadControlLimits + // + // parameters: + // - entity: the entity of the e.g. EVSE + // - limit: load limit data + WriteConsumptionLimit(entity spineapi.EntityRemoteInterface, limit api.LoadLimit) (*model.MsgCounterType, error) + + // Scenario 2 + + // return Failsafe limit for the consumed active (real) power of the + // Controllable System. This limit becomes activated in "init" state or "failsafe state". + // + // parameters: + // - entity: the entity of the e.g. EVSE + // + // return values: + // - positive values are used for consumption + FailsafeConsumptionActivePowerLimit(entity spineapi.EntityRemoteInterface) (float64, error) + + // send new Failsafe Consumption Active Power Limit + // + // parameters: + // - entity: the entity of the e.g. EVSE + // - value: the new limit in W + WriteFailsafeConsumptionActivePowerLimit(entity spineapi.EntityRemoteInterface, value float64) (*model.MsgCounterType, error) + + // return minimum time the Controllable System remains in "failsafe state" unless conditions + // specified in this Use Case permit leaving the "failsafe state" + // + // parameters: + // - entity: the entity of the e.g. EVSE + // + // return values: + // - negative values are used for production + FailsafeDurationMinimum(entity spineapi.EntityRemoteInterface) (time.Duration, error) + + // send new Failsafe Duration Minimum + // + // parameters: + // - entity: the entity of the e.g. EVSE + // - duration: the duration, between 2h and 24h + WriteFailsafeDurationMinimum(entity spineapi.EntityRemoteInterface, duration time.Duration) (*model.MsgCounterType, error) + + // Scenario 3 + + // this is automatically covered by the SPINE implementation + + // Scenario 4 + + // return nominal maximum active (real) power the Controllable System is + // able to consume according to the device label or data sheet. + // + // parameters: + // - entity: the entity of the e.g. EVSE + PowerConsumptionNominalMax(entity spineapi.EntityRemoteInterface) (float64, error) +} diff --git a/uclpc/events.go b/uclpc/events.go new file mode 100644 index 0000000..218f1fb --- /dev/null +++ b/uclpc/events.go @@ -0,0 +1,99 @@ +package uclpc + +import ( + "github.com/enbility/cemd/util" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// handle SPINE events +func (e *UCLPC) HandleEvent(payload spineapi.EventPayload) { + if !util.IsCompatibleEntity(payload.Entity, e.validEntityTypes) { + return + } + + if util.IsEntityConnected(payload) { + e.connected(payload.Entity) + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + + switch payload.Data.(type) { + case *model.LoadControlLimitDescriptionListDataType: + e.loadControlLimitDescriptionDataUpdate(payload.Entity) + case *model.LoadControlLimitListDataType: + e.loadControlLimitDataUpdate(payload) + case *model.DeviceConfigurationKeyValueDescriptionListDataType: + e.configurationDescriptionDataUpdate(payload.Entity) + case *model.DeviceConfigurationKeyValueListDataType: + e.configurationDataUpdate(payload) + } +} + +// the remote entity was connected +func (e *UCLPC) connected(entity spineapi.EntityRemoteInterface) { + // initialise features, e.g. subscriptions, descriptions + if loadControl, err := util.LoadControl(e.service, entity); err == nil { + if _, err := loadControl.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + // get descriptions + if _, err := loadControl.RequestLimitDescriptions(); err != nil { + logging.Log().Debug(err) + } + } + + if localDeviceDiag, err := util.DeviceDiagnosis(e.service, entity); err == nil { + if _, err := localDeviceDiag.Subscribe(); err != nil { + logging.Log().Debug(err) + } + } +} + +// the load control limit description data was updated +func (e *UCLPC) loadControlLimitDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if loadControl, err := util.LoadControl(e.service, entity); err == nil { + // get values + if _, err := loadControl.RequestLimitValues(); err != nil { + logging.Log().Debug(err) + } + } +} + +// the load control limit data was updated +func (e *UCLPC) loadControlLimitDataUpdate(payload spineapi.EventPayload) { + if util.LoadControlLimitsCheckPayloadDataForTypeCategoryDirectionScope( + false, e.service, payload, + model.LoadControlLimitTypeTypeSignDependentAbsValueLimit, + model.LoadControlCategoryTypeObligation, + model.EnergyDirectionTypeConsume, + model.ScopeTypeTypeActivePowerLimit) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateLimit) + } +} + +// the configuration key description data was updated +func (e *UCLPC) configurationDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if deviceConfiguration, err := util.DeviceConfiguration(e.service, entity); err == nil { + // key value descriptions received, now get the data + if _, err := deviceConfiguration.RequestKeyValues(); err != nil { + logging.Log().Error("Error getting configuration key values:", err) + } + } +} + +// the configuration key data was updated +func (e *UCLPC) configurationDataUpdate(payload spineapi.EventPayload) { + if util.DeviceConfigurationCheckDataPayloadForKeyName(false, e.service, payload, model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateFailsafeConsumptionActivePowerLimit) + } + if util.DeviceConfigurationCheckDataPayloadForKeyName(false, e.service, payload, model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateFailsafeDurationMinimum) + } +} diff --git a/uclpc/events_test.go b/uclpc/events_test.go new file mode 100644 index 0000000..6098d62 --- /dev/null +++ b/uclpc/events_test.go @@ -0,0 +1,147 @@ +package uclpc + +import ( + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCLPCSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.monitoredEntity + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeEntityChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.Data = eebusutil.Ptr(model.LoadControlLimitDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.LoadControlLimitListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.DeviceConfigurationKeyValueDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.DeviceConfigurationKeyValueListDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *UCLPCSuite) Test_Failures() { + s.sut.connected(s.mockRemoteEntity) + + s.sut.configurationDescriptionDataUpdate(s.mockRemoteEntity) +} + +func (s *UCLPCSuite) Test_loadControlLimitDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.monitoredEntity, + } + s.sut.loadControlLimitDataUpdate(payload) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + LimitType: eebusutil.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitCategory: eebusutil.Ptr(model.LoadControlCategoryTypeObligation), + LimitDirection: eebusutil.Ptr(model.EnergyDirectionTypeConsume), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeActivePowerLimit), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeLoadControlLimitDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.loadControlLimitDataUpdate(payload) + + data := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{}, + } + + payload.Data = data + + s.sut.loadControlLimitDataUpdate(payload) + + data = &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + Value: model.NewScaledNumberType(16), + }, + }, + } + + payload.Data = data + + s.sut.loadControlLimitDataUpdate(payload) +} + +func (s *UCLPCSuite) Test_configurationDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.monitoredEntity, + } + s.sut.configurationDataUpdate(payload) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(1)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit), + }, + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(2)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.configurationDataUpdate(payload) + + data := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{}, + } + + payload.Data = data + + s.sut.configurationDataUpdate(payload) + + data = &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(1)), + Value: &model.DeviceConfigurationKeyValueValueType{}, + }, + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(2)), + Value: &model.DeviceConfigurationKeyValueValueType{}, + }, + }, + } + + payload.Data = data + + s.sut.configurationDataUpdate(payload) +} diff --git a/uclpc/public.go b/uclpc/public.go new file mode 100644 index 0000000..267b19d --- /dev/null +++ b/uclpc/public.go @@ -0,0 +1,304 @@ +package uclpc + +import ( + "errors" + "time" + + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// Scenario 1 + +// return the current loadcontrol limit data +// +// parameters: +// - entity: the entity of the e.g. EVSE +// +// return values: +// - limit: load limit data +// +// possible errors: +// - ErrDataNotAvailable if no such limit is (yet) available +// - and others +func (e *UCLPC) ConsumptionLimit(entity spineapi.EntityRemoteInterface) ( + limit api.LoadLimit, resultErr error) { + limit = api.LoadLimit{ + Value: 0.0, + IsChangeable: false, + IsActive: false, + } + + resultErr = api.ErrNoCompatibleEntity + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return + } + + resultErr = eebusapi.ErrDataNotAvailable + loadControl, err := util.LoadControl(e.service, entity) + if err != nil || loadControl == nil { + return + } + + limitDescriptions, err := loadControl.GetLimitDescriptionsForTypeCategoryDirectionScope( + model.LoadControlLimitTypeTypeSignDependentAbsValueLimit, + model.LoadControlCategoryTypeObligation, + model.EnergyDirectionTypeConsume, + model.ScopeTypeTypeActivePowerLimit) + if err != nil || len(limitDescriptions) != 1 { + return + } + + value, err := loadControl.GetLimitValueForLimitId(*limitDescriptions[0].LimitId) + if err != nil || value.Value == nil { + return + } + + limit.Value = value.Value.GetValue() + limit.IsChangeable = (value.IsLimitChangeable != nil && *value.IsLimitChangeable) + limit.IsActive = (value.IsLimitActive != nil && *value.IsLimitActive) + if value.TimePeriod != nil && value.TimePeriod.EndTime != nil { + if duration, err := value.TimePeriod.EndTime.GetTimeDuration(); err == nil { + limit.Duration = duration + } + } + + resultErr = nil + + return +} + +// send new LoadControlLimits +// +// parameters: +// - entity: the entity of the e.g. EVSE +// - limit: load limit data +func (e *UCLPC) WriteConsumptionLimit( + entity spineapi.EntityRemoteInterface, + limit api.LoadLimit) (*model.MsgCounterType, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return nil, api.ErrNoCompatibleEntity + } + + loadControl, err := util.LoadControl(e.service, entity) + if err != nil { + return nil, api.ErrNoCompatibleEntity + } + + var limitData []model.LoadControlLimitDataType + + limitDescriptions, err := loadControl.GetLimitDescriptionsForTypeCategoryDirectionScope( + model.LoadControlLimitTypeTypeSignDependentAbsValueLimit, + model.LoadControlCategoryTypeObligation, + model.EnergyDirectionTypeConsume, + model.ScopeTypeTypeActivePowerLimit, + ) + if err != nil || + len(limitDescriptions) != 1 || + limitDescriptions[0].LimitId == nil { + return nil, eebusapi.ErrMetadataNotAvailable + } + + limitDesc := limitDescriptions[0] + + if _, err := loadControl.GetLimitValueForLimitId(*limitDesc.LimitId); err != nil { + return nil, eebusapi.ErrDataNotAvailable + } + + currentLimits, err := loadControl.GetLimitValues() + if err != nil { + return nil, eebusapi.ErrDataNotAvailable + } + + for index, item := range currentLimits { + if item.LimitId == nil || + *item.LimitId != *limitDesc.LimitId { + continue + } + + // EEBus_UC_TS_LimitationOfPowerConsumption V1.0.0 3.2.2.2.2.2 + // If set to "true", the timePeriod, value and isLimitActive Elements SHALL be writeable by a client. + if item.IsLimitChangeable != nil && !*item.IsLimitChangeable { + return nil, eebusapi.ErrNotSupported + } + + newLimit := model.LoadControlLimitDataType{ + LimitId: limitDesc.LimitId, + IsLimitActive: eebusutil.Ptr(limit.IsActive), + Value: model.NewScaledNumberType(limit.Value), + } + if limit.Duration > 0 { + newLimit.TimePeriod = &model.TimePeriodType{ + EndTime: model.NewAbsoluteOrRelativeTimeTypeFromDuration(limit.Duration), + } + } + + currentLimits[index] = newLimit + break + } + + msgCounter, err := loadControl.WriteLimitValues(limitData) + + return msgCounter, err +} + +// Scenario 2 + +// return Failsafe limit for the consumed active (real) power of the +// Controllable System. This limit becomes activated in "init" state or "failsafe state". +func (e *UCLPC) FailsafeConsumptionActivePowerLimit(entity spineapi.EntityRemoteInterface) (float64, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + + keyname := model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit + + deviceConfiguration, err := util.DeviceConfiguration(e.service, entity) + if err != nil || deviceConfiguration == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + data, err := deviceConfiguration.GetKeyValueForKeyName(keyname, model.DeviceConfigurationKeyValueTypeTypeScaledNumber) + if err != nil || data == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + value, ok := data.(*model.ScaledNumberType) + if !ok || value == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + return value.GetValue(), nil +} + +// send new Failsafe Consumption Active Power Limit +// +// parameters: +// - entity: the entity of the e.g. EVSE +// - value: the new limit in W +func (e *UCLPC) WriteFailsafeConsumptionActivePowerLimit(entity spineapi.EntityRemoteInterface, value float64) (*model.MsgCounterType, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return nil, api.ErrNoCompatibleEntity + } + + keyname := model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit + + deviceConfiguration, err := util.DeviceConfiguration(e.service, entity) + if err != nil || deviceConfiguration == nil { + return nil, eebusapi.ErrDataNotAvailable + } + + data, err := deviceConfiguration.GetDescriptionForKeyName(keyname) + if err != nil || data == nil || data.KeyId == nil { + return nil, eebusapi.ErrDataNotAvailable + } + + keyData := []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: data.KeyId, + Value: &model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(value), + }, + }, + } + + msgCounter, err := deviceConfiguration.WriteKeyValues(keyData) + + return msgCounter, err +} + +// return minimum time the Controllable System remains in "failsafe state" unless conditions +// specified in this Use Case permit leaving the "failsafe state" +func (e *UCLPC) FailsafeDurationMinimum(entity spineapi.EntityRemoteInterface) (time.Duration, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + + keyname := model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum + + deviceConfiguration, err := util.DeviceConfiguration(e.service, entity) + if err != nil || deviceConfiguration == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + data, err := deviceConfiguration.GetKeyValueForKeyName(keyname, model.DeviceConfigurationKeyValueTypeTypeDuration) + if err != nil || data == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + value, ok := data.(*model.DurationType) + if !ok || value == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + return value.GetTimeDuration() +} + +// send new Failsafe Duration Minimum +// +// parameters: +// - entity: the entity of the e.g. EVSE +// - duration: the duration, between 2h and 24h +func (e *UCLPC) WriteFailsafeDurationMinimum(entity spineapi.EntityRemoteInterface, duration time.Duration) (*model.MsgCounterType, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return nil, api.ErrNoCompatibleEntity + } + + if duration < time.Duration(time.Hour*2) || duration > time.Duration(time.Hour*24) { + return nil, errors.New("duration outside of allowed range") + } + + keyname := model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum + + deviceConfiguration, err := util.DeviceConfiguration(e.service, entity) + if err != nil || deviceConfiguration == nil { + return nil, eebusapi.ErrDataNotAvailable + } + + data, err := deviceConfiguration.GetDescriptionForKeyName(keyname) + if err != nil || data == nil || data.KeyId == nil { + return nil, eebusapi.ErrDataNotAvailable + } + + keyData := []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: data.KeyId, + Value: &model.DeviceConfigurationKeyValueValueType{ + Duration: model.NewDurationType(duration), + }, + }, + } + + msgCounter, err := deviceConfiguration.WriteKeyValues(keyData) + + return msgCounter, err +} + +// Scenario 4 + +// return nominal maximum active (real) power the Controllable System is +// able to consume according to the device label or data sheet. +func (e *UCLPC) PowerConsumptionNominalMax(entity spineapi.EntityRemoteInterface) (float64, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + + electricalConnection, err := util.ElectricalConnection(e.service, entity) + if err != nil || electricalConnection == nil { + return 0, err + } + + data, err := electricalConnection.GetCharacteristicForContextType( + model.ElectricalConnectionCharacteristicContextTypeEntity, + model.ElectricalConnectionCharacteristicTypeTypePowerConsumptionNominalMax, + ) + if err != nil || data.Value == nil { + return 0, err + } + + return data.Value.GetValue(), nil +} diff --git a/uclpc/public_test.go b/uclpc/public_test.go new file mode 100644 index 0000000..2416942 --- /dev/null +++ b/uclpc/public_test.go @@ -0,0 +1,355 @@ +package uclpc + +import ( + "time" + + "github.com/enbility/cemd/api" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCLPCSuite) Test_LoadControlLimit() { + data, err := s.sut.ConsumptionLimit(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data.Value) + assert.Equal(s.T(), false, data.IsChangeable) + assert.Equal(s.T(), false, data.IsActive) + + data, err = s.sut.ConsumptionLimit(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data.Value) + assert.Equal(s.T(), false, data.IsChangeable) + assert.Equal(s.T(), false, data.IsActive) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + LimitCategory: eebusutil.Ptr(model.LoadControlCategoryTypeObligation), + LimitType: eebusutil.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitDirection: eebusutil.Ptr(model.EnergyDirectionTypeConsume), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeActivePowerLimit), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeLoadControlLimitDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.ConsumptionLimit(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data.Value) + assert.Equal(s.T(), false, data.IsChangeable) + assert.Equal(s.T(), false, data.IsActive) + + limitData := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + IsLimitChangeable: eebusutil.Ptr(true), + IsLimitActive: eebusutil.Ptr(false), + Value: model.NewScaledNumberType(6000), + TimePeriod: &model.TimePeriodType{ + EndTime: model.NewAbsoluteOrRelativeTimeType("PT2H"), + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeLoadControlLimitListData, limitData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.ConsumptionLimit(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 6000.0, data.Value) + assert.Equal(s.T(), true, data.IsChangeable) + assert.Equal(s.T(), false, data.IsActive) +} + +func (s *UCLPCSuite) Test_WriteLoadControlLimit() { + limit := api.LoadLimit{ + Value: 6000, + IsActive: true, + Duration: 0, + } + _, err := s.sut.WriteConsumptionLimit(s.mockRemoteEntity, limit) + assert.NotNil(s.T(), err) + + _, err = s.sut.WriteConsumptionLimit(s.monitoredEntity, limit) + assert.NotNil(s.T(), err) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + LimitCategory: eebusutil.Ptr(model.LoadControlCategoryTypeObligation), + LimitType: eebusutil.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitDirection: eebusutil.Ptr(model.EnergyDirectionTypeConsume), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeActivePowerLimit), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeLoadControlLimitDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.WriteConsumptionLimit(s.monitoredEntity, limit) + assert.NotNil(s.T(), err) + + limitData := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + IsLimitChangeable: eebusutil.Ptr(true), + IsLimitActive: eebusutil.Ptr(false), + Value: model.NewScaledNumberType(6000), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeLoadControlLimitListData, limitData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.WriteConsumptionLimit(s.monitoredEntity, limit) + assert.NotNil(s.T(), err) + + limit.Duration = time.Duration(time.Hour * 2) + _, err = s.sut.WriteConsumptionLimit(s.monitoredEntity, limit) + assert.NotNil(s.T(), err) +} + +func (s *UCLPCSuite) Test_FailsafeConsumptionActivePowerLimit() { + data, err := s.sut.FailsafeConsumptionActivePowerLimit(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.FailsafeConsumptionActivePowerLimit(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.FailsafeConsumptionActivePowerLimit(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{}, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.FailsafeConsumptionActivePowerLimit(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + keyData = &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(4000), + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.FailsafeConsumptionActivePowerLimit(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 4000.0, data) +} + +func (s *UCLPCSuite) Test_WriteFailsafeConsumptionActivePowerLimit() { + _, err := s.sut.WriteFailsafeConsumptionActivePowerLimit(s.mockRemoteEntity, 6000) + assert.NotNil(s.T(), err) + + _, err = s.sut.WriteFailsafeConsumptionActivePowerLimit(s.monitoredEntity, 6000) + assert.NotNil(s.T(), err) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.WriteFailsafeConsumptionActivePowerLimit(s.monitoredEntity, 6000) + assert.Nil(s.T(), err) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{}, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.WriteFailsafeConsumptionActivePowerLimit(s.monitoredEntity, 6000) + assert.Nil(s.T(), err) +} + +func (s *UCLPCSuite) Test_FailsafeDurationMinimum() { + data, err := s.sut.FailsafeDurationMinimum(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), time.Duration(0), data) + + data, err = s.sut.FailsafeDurationMinimum(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), time.Duration(0), data) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.FailsafeDurationMinimum(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), time.Duration(0), data) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{}, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.FailsafeDurationMinimum(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), time.Duration(0), data) + + keyData = &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + Duration: model.NewDurationType(time.Hour * 2), + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.FailsafeDurationMinimum(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), time.Duration(time.Hour*2), data) +} + +func (s *UCLPCSuite) Test_WriteFailsafeDurationMinimum() { + _, err := s.sut.WriteFailsafeDurationMinimum(s.mockRemoteEntity, time.Duration(time.Hour*2)) + assert.NotNil(s.T(), err) + + _, err = s.sut.WriteFailsafeDurationMinimum(s.monitoredEntity, time.Duration(time.Hour*2)) + assert.NotNil(s.T(), err) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.WriteFailsafeDurationMinimum(s.monitoredEntity, time.Duration(time.Hour*2)) + assert.Nil(s.T(), err) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{}, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.WriteFailsafeDurationMinimum(s.monitoredEntity, time.Duration(time.Hour*2)) + assert.Nil(s.T(), err) + + _, err = s.sut.WriteFailsafeDurationMinimum(s.monitoredEntity, time.Duration(time.Hour*1)) + assert.NotNil(s.T(), err) +} + +func (s *UCLPCSuite) Test_PowerConsumptionNominalMax() { + data, err := s.sut.PowerConsumptionNominalMax(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.PowerConsumptionNominalMax(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + charData := &model.ElectricalConnectionCharacteristicListDataType{ + ElectricalConnectionCharacteristicData: []model.ElectricalConnectionCharacteristicDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + CharacteristicId: eebusutil.Ptr(model.ElectricalConnectionCharacteristicIdType(0)), + CharacteristicContext: eebusutil.Ptr(model.ElectricalConnectionCharacteristicContextTypeEntity), + CharacteristicType: eebusutil.Ptr(model.ElectricalConnectionCharacteristicTypeTypePowerConsumptionNominalMax), + Value: model.NewScaledNumberType(8000), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionCharacteristicListData, charData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerConsumptionNominalMax(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 8000.0, data) +} diff --git a/uclpc/testhelper_test.go b/uclpc/testhelper_test.go new file mode 100644 index 0000000..1f58fb9 --- /dev/null +++ b/uclpc/testhelper_test.go @@ -0,0 +1,182 @@ +package uclpc + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/cemd/api" + eebusapi "github.com/enbility/eebus-go/api" + eebusmocks "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestLPCSuite(t *testing.T) { + suite.Run(t, new(UCLPCSuite)) +} + +type UCLPCSuite struct { + suite.Suite + + sut *UCLPC + + service eebusapi.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *mocks.EntityRemoteInterface + monitoredEntity spineapi.EntityRemoteInterface +} + +func (s *UCLPCSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { +} + +func (s *UCLPCSuite) BeforeTest(suiteName, testName string) { + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := eebusapi.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := eebusmocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := mocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = mocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := mocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + s.sut = NewUCLPC(s.service, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + s.remoteDevice, s.monitoredEntity = setupDevices(s.service, s.T()) +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService eebusapi.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + role model.RoleType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeLoadControl, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeLoadControlLimitDescriptionListData, + model.FunctionTypeLoadControlLimitListData, + }, + }, + {model.FeatureTypeTypeDeviceConfiguration, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, + model.FunctionTypeDeviceConfigurationKeyValueListData, + }, + }, + {model.FeatureTypeTypeDeviceDiagnosis, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeDeviceDiagnosisHeartbeatData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionCharacteristicListData, + }, + }, + } + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: eebusutil.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + Feature: eebusutil.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: eebusutil.Ptr(feature.featureType), + Role: eebusutil.Ptr(feature.role), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeEVSE), + }, + }, + }, + FeatureInformation: featureInformations, + } + + entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + remoteDevice.UpdateDevice(detailedData.DeviceInformation.Description) + + localDevice.AddRemoteDeviceForSki(remoteSki, remoteDevice) + + return remoteDevice, entities[0] +} diff --git a/uclpc/types.go b/uclpc/types.go new file mode 100644 index 0000000..82550e8 --- /dev/null +++ b/uclpc/types.go @@ -0,0 +1,28 @@ +package uclpc + +import "github.com/enbility/cemd/api" + +const ( + // Load control obligation limit data updated + // + // Use `ConsumptionLimit` to get the current data + // + // Use Case LPC, Scenario 1 + DataUpdateLimit api.EventType = "DataUpdateLimit" + + // Failsafe limit for the consumed active (real) power of the + // Controllable System data updated + // + // Use `FailsafeConsumptionActivePowerLimit` to get the current data + // + // Use Case LPC, Scenario 2 + DataUpdateFailsafeConsumptionActivePowerLimit api.EventType = "DataUpdateFailsafeConsumptionActivePowerLimit" + + // Minimum time the Controllable System remains in "failsafe state" unless conditions + // specified in this Use Case permit leaving the "failsafe state" data updated + // + // Use `FailsafeDurationMinimum` to get the current data + // + // Use Case LPC, Scenario 2 + DataUpdateFailsafeDurationMinimum api.EventType = "DataUpdateFailsafeDurationMinimum" +) diff --git a/uclpc/uclpc.go b/uclpc/uclpc.go new file mode 100644 index 0000000..6311ba0 --- /dev/null +++ b/uclpc/uclpc.go @@ -0,0 +1,121 @@ +package uclpc + +import ( + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +type UCLPC struct { + service eebusapi.ServiceInterface + + eventCB api.EntityEventCallback + + validEntityTypes []model.EntityTypeType +} + +var _ UCLPCInterface = (*UCLPC)(nil) + +func NewUCLPC(service eebusapi.ServiceInterface, eventCB api.EntityEventCallback) *UCLPC { + uc := &UCLPC{ + service: service, + eventCB: eventCB, + } + + uc.validEntityTypes = []model.EntityTypeType{ + model.EntityTypeTypeCompressor, + model.EntityTypeTypeEVSE, + model.EntityTypeTypeHeatPumpAppliance, + model.EntityTypeTypeInverter, + model.EntityTypeTypeSmartEnergyAppliance, + model.EntityTypeTypeSubMeterElectricity, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (c *UCLPC) UseCaseName() model.UseCaseNameType { + return model.UseCaseNameTypeLimitationOfPowerConsumption +} + +func (e *UCLPC) AddFeatures() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceDiagnosis, + model.FeatureTypeTypeLoadControl, + model.FeatureTypeTypeDeviceConfiguration, + model.FeatureTypeTypeElectricalConnection, + } + for _, feature := range clientFeatures { + _ = localEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } + + // server features + f := localEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeDeviceDiagnosisHeartbeatData, true, false) +} + +func (e *UCLPC) AddUseCase() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.AddUseCaseSupport( + model.UseCaseActorTypeEnergyGuard, + e.UseCaseName(), + model.SpecificationVersionType("1.0.0"), + "release", + true, + []model.UseCaseScenarioSupportType{1, 2, 3, 4}) +} + +func (e *UCLPC) UpdateUseCaseAvailability(available bool) { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.SetUseCaseAvailability(model.UseCaseActorTypeEnergyGuard, e.UseCaseName(), available) +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *UCLPC) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return false, api.ErrNoCompatibleEntity + } + + // check if the usecase and mandatory scenarios are supported and + // if the required server features are available + if !entity.Device().VerifyUseCaseScenariosAndFeaturesSupport( + model.UseCaseActorTypeEnergyGuard, + e.UseCaseName(), + []model.UseCaseScenarioSupportType{1, 2, 3, 4}, + []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceDiagnosis, + model.FeatureTypeTypeLoadControl, + model.FeatureTypeTypeDeviceConfiguration, + }, + ) { + return false, nil + } + + if _, err := util.DeviceDiagnosis(e.service, entity); err != nil { + return false, eebusapi.ErrFunctionNotSupported + } + + if _, err := util.LoadControl(e.service, entity); err != nil { + return false, eebusapi.ErrFunctionNotSupported + } + + if _, err := util.DeviceConfiguration(e.service, entity); err != nil { + return false, eebusapi.ErrFunctionNotSupported + } + + return true, nil +} diff --git a/uclpc/uclpc_test.go b/uclpc/uclpc_test.go new file mode 100644 index 0000000..56188de --- /dev/null +++ b/uclpc/uclpc_test.go @@ -0,0 +1,45 @@ +package uclpc + +import ( + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCLPCSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *UCLPCSuite) Test_IsUseCaseSupported() { + data, err := s.sut.IsUseCaseSupported(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + data, err = s.sut.IsUseCaseSupported(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: eebusutil.Ptr(model.UseCaseActorTypeEnergyGuard), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: eebusutil.Ptr(model.UseCaseNameTypeLimitationOfPowerConsumption), + UseCaseAvailable: eebusutil.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1, 2, 3, 4}, + }, + }, + }, + }, + } + + nodemgmtEntity := s.remoteDevice.Entity([]model.AddressEntityType{0}) + nodeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(nodemgmtEntity, model.FeatureTypeTypeNodeManagement, model.RoleTypeSpecial) + fErr := nodeFeature.UpdateData(model.FunctionTypeNodeManagementUseCaseData, ucData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/uclpcserver/api.go b/uclpcserver/api.go new file mode 100644 index 0000000..84ba0da --- /dev/null +++ b/uclpcserver/api.go @@ -0,0 +1,92 @@ +package uclpcserver + +import ( + "time" + + "github.com/enbility/cemd/api" + "github.com/enbility/spine-go/model" +) + +//go:generate mockery + +// interface for the Limitation of Power Consumption UseCase as a server +type UCLPCServerInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // return the current consumption limit data + // + // return values: + // - limit: load limit data + // + // possible errors: + // - ErrDataNotAvailable if no such limit is (yet) available + // - and others + ConsumptionLimit() (api.LoadLimit, error) + + // set the current loadcontrol limit data + SetConsumptionLimit(limit api.LoadLimit) (resultErr error) + + // return the currently pending incoming consumption write limits + PendingConsumptionLimits() map[model.MsgCounterType]api.LoadLimit + + // accept or deny an incoming consumption write limit + // + // parameters: + // - msg: the incoming write message + // - approve: if the write limit for msg should be approved or not + // - reason: the reason why the approval is denied, otherwise an empty string + ApproveOrDenyConsumptionLimit(msgCounter model.MsgCounterType, approve bool, reason string) + + // Scenario 2 + + // return Failsafe limit for the consumed active (real) power of the + // Controllable System. This limit becomes activated in "init" state or "failsafe state". + // + // return values: + // - value: the power limit in W + // - changeable: boolean if the client service can change the limit + FailsafeConsumptionActivePowerLimit() (value float64, isChangeable bool, resultErr error) + + // set Failsafe limit for the consumed active (real) power of the + // Controllable System. This limit becomes activated in "init" state or "failsafe state". + // + // parameters: + // - value: the power limit in W + // - changeable: boolean if the client service can change the limit + SetFailsafeConsumptionActivePowerLimit(value float64, changeable bool) (resultErr error) + + // return minimum time the Controllable System remains in "failsafe state" unless conditions + // specified in this Use Case permit leaving the "failsafe state" + // + // return values: + // - value: the power limit in W + // - changeable: boolean if the client service can change the limit + FailsafeDurationMinimum() (duration time.Duration, isChangeable bool, resultErr error) + + // set minimum time the Controllable System remains in "failsafe state" unless conditions + // specified in this Use Case permit leaving the "failsafe state" + // + // parameters: + // - duration: has to be >= 2h and <= 24h + // - changeable: boolean if the client service can change this value + SetFailsafeDurationMinimum(duration time.Duration, changeable bool) (resultErr error) + + // Scenario 3 + + // this is automatically covered by the SPINE implementation + + // Scenario 4 + + // return nominal maximum active (real) power the Controllable System is + // allowed to consume due to the customer's contract. + ContractualConsumptionNominalMax() (float64, error) + + // set nominal maximum active (real) power the Controllable System is + // allowed to consume due to the customer's contract. + // + // parameters: + // - value: contractual nominal max power consumption in W + SetContractualConsumptionNominalMax(value float64) (resultErr error) +} diff --git a/uclpcserver/events.go b/uclpcserver/events.go new file mode 100644 index 0000000..1619a77 --- /dev/null +++ b/uclpcserver/events.go @@ -0,0 +1,157 @@ +package uclpcserver + +import ( + "slices" + + "github.com/enbility/cemd/util" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// handle SPINE events +func (e *UCLPCServer) HandleEvent(payload spineapi.EventPayload) { + if util.IsDeviceConnected(payload) { + e.deviceConnected(payload) + return + } + + if !util.IsCompatibleEntity(payload.Entity, e.validEntityTypes) { + return + } + + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + // did we receive a binding to the loadControl server and the + // heartbeatWorkaround is required? + if payload.EventType == spineapi.EventTypeBindingChange && + payload.ChangeType == spineapi.ElementChangeAdd && + payload.LocalFeature != nil && + payload.LocalFeature.Type() == model.FeatureTypeTypeLoadControl && + payload.LocalFeature.Role() == model.RoleTypeServer { + e.subscribeHeartbeatWorkaround(payload) + return + } + + if localEntity == nil || + payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate || + payload.CmdClassifier == nil || + *payload.CmdClassifier != model.CmdClassifierTypeWrite { + return + } + + // the codefactor warning is invalid, as .(type) check can not be replaced with if then + //revive:disable-next-line + switch payload.Data.(type) { + case *model.LoadControlLimitListDataType: + serverF := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + + if payload.Function != model.FunctionTypeLoadControlLimitListData || + payload.LocalFeature != serverF { + return + } + + e.loadControlLimitDataUpdate(payload) + case *model.DeviceConfigurationKeyValueListDataType: + serverF := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + + if payload.Function != model.FunctionTypeDeviceConfigurationKeyValueListData || + payload.LocalFeature != serverF { + return + } + + e.configurationDataUpdate(payload) + } +} + +// a remote device was connected and we know its entities +func (e *UCLPCServer) deviceConnected(payload spineapi.EventPayload) { + if payload.Device == nil { + return + } + + // check if there is a DeviceDiagnosis server on one or more entities + remoteDevice := payload.Device + + var deviceDiagEntites []spineapi.EntityRemoteInterface + + entites := remoteDevice.Entities() + for _, entity := range entites { + if !slices.Contains(e.validEntityTypes, entity.EntityType()) { + continue + } + + deviceDiagF := entity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + if deviceDiagF == nil { + continue + } + + deviceDiagEntites = append(deviceDiagEntites, entity) + } + + // the remote device does not have a DeviceDiagnosis Server, which it should + if len(deviceDiagEntites) == 0 { + return + } + + // we only found one matching entity, as it should be, subscribe + if len(deviceDiagEntites) == 1 { + if localDeviceDiag, err := util.DeviceDiagnosis(e.service, deviceDiagEntites[0]); err == nil { + if _, err := localDeviceDiag.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + if _, err := localDeviceDiag.RequestHeartbeat(); err != nil { + logging.Log().Debug(err) + } + } + + return + } + + // we found more than one matching entity, this is not good + // according to KEO the subscription should be done on the entity that requests a binding to + // the local loadControlLimit server feature + e.heartbeatKeoWorkaround = true +} + +// subscribe to the DeviceDiagnosis Server of the entity that created a binding +func (e *UCLPCServer) subscribeHeartbeatWorkaround(payload spineapi.EventPayload) { + // the workaround is not needed, exit + if !e.heartbeatKeoWorkaround { + return + } + + if localDeviceDiag, err := util.DeviceDiagnosis(e.service, payload.Entity); err == nil { + if _, err := localDeviceDiag.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + if _, err := localDeviceDiag.RequestHeartbeat(); err != nil { + logging.Log().Debug(err) + } + } +} + +// the load control limit data was updated +func (e *UCLPCServer) loadControlLimitDataUpdate(payload spineapi.EventPayload) { + if util.LoadControlLimitsCheckPayloadDataForTypeCategoryDirectionScope( + true, e.service, payload, + model.LoadControlLimitTypeTypeSignDependentAbsValueLimit, + model.LoadControlCategoryTypeObligation, + model.EnergyDirectionTypeConsume, + model.ScopeTypeTypeActivePowerLimit) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateLimit) + } +} + +// the configuration key data of an SMGW was updated +func (e *UCLPCServer) configurationDataUpdate(payload spineapi.EventPayload) { + if util.DeviceConfigurationCheckDataPayloadForKeyName(true, e.service, payload, model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateFailsafeConsumptionActivePowerLimit) + } + if util.DeviceConfigurationCheckDataPayloadForKeyName(true, e.service, payload, model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateFailsafeDurationMinimum) + } +} diff --git a/uclpcserver/events_test.go b/uclpcserver/events_test.go new file mode 100644 index 0000000..b3706bc --- /dev/null +++ b/uclpcserver/events_test.go @@ -0,0 +1,309 @@ +package uclpcserver + +import ( + "fmt" + + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" +) + +func (s *UCLPCServerSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Device = s.monitoredEntity.Device() + payload.Entity = s.monitoredEntity + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDeviceChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeEntityChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.CmdClassifier = eebusutil.Ptr(model.CmdClassifierTypeWrite) + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.Function = model.FunctionTypeLoadControlLimitListData + payload.Data = eebusutil.Ptr(model.LoadControlLimitListDataType{}) + s.sut.HandleEvent(payload) + + payload.LocalFeature = s.loadControlFeature + s.sut.HandleEvent(payload) + + payload.Function = model.FunctionTypeDeviceConfigurationKeyValueListData + payload.Data = eebusutil.Ptr(model.DeviceConfigurationKeyValueListDataType{}) + s.sut.HandleEvent(payload) + + payload.LocalFeature = s.deviceConfigurationFeature + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeBindingChange + payload.ChangeType = spineapi.ElementChangeAdd + payload.LocalFeature = s.loadControlFeature + s.sut.HandleEvent(payload) +} + +func (s *UCLPCServerSuite) Test_deviceConnected() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + + s.sut.deviceConnected(payload) + + // no entities + mockRemoteDevice := mocks.NewDeviceRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().Entities().Return(nil) + payload.Device = mockRemoteDevice + s.sut.deviceConnected(payload) + + // one entity with one DeviceDiagnosis server + payload.Device = s.remoteDevice + s.sut.deviceConnected(payload) + + s.sut.subscribeHeartbeatWorkaround(payload) +} + +func (s *UCLPCServerSuite) Test_multipleDeviceDiagServer() { + // multiple entities each with DeviceDiagnosis server + + payload := spineapi.EventPayload{ + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + role model.RoleType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeLoadControl, + model.RoleTypeClient, + []model.FunctionType{}, + }, + {model.FeatureTypeTypeDeviceConfiguration, + model.RoleTypeClient, + []model.FunctionType{}, + }, + {model.FeatureTypeTypeDeviceDiagnosis, + model.RoleTypeClient, + []model.FunctionType{}, + }, + {model.FeatureTypeTypeDeviceDiagnosis, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeDeviceDiagnosisHeartbeatData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + model.RoleTypeClient, + []model.FunctionType{}, + }, + } + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + // 4 entites + for i := 1; i < 5; i++ { + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: eebusutil.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{model.AddressEntityType(i)}, + Feature: eebusutil.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: eebusutil.Ptr(feature.featureType), + Role: eebusutil.Ptr(feature.role), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeCEM), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{2}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeCEM), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{3}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeCEM), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{4}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeCEM), + }, + }, + }, + FeatureInformation: featureInformations, + } + + _, err := s.remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + s.remoteDevice.UpdateDevice(detailedData.DeviceInformation.Description) + + s.sut.deviceConnected(payload) + + s.sut.subscribeHeartbeatWorkaround(payload) +} + +func (s *UCLPCServerSuite) Test_loadControlLimitDataUpdate() { + localDevice := s.service.LocalDevice() + localEntity := localDevice.EntityForType(model.EntityTypeTypeCEM) + + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.monitoredEntity, + } + s.sut.loadControlLimitDataUpdate(payload) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + LimitType: eebusutil.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitCategory: eebusutil.Ptr(model.LoadControlCategoryTypeObligation), + LimitDirection: eebusutil.Ptr(model.EnergyDirectionTypeConsume), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeActivePowerLimit), + }, + }, + } + + lFeature := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + lFeature.SetData(model.FunctionTypeLoadControlLimitDescriptionListData, descData) + + s.sut.loadControlLimitDataUpdate(payload) + + data := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{}, + } + + payload.Data = data + + s.sut.loadControlLimitDataUpdate(payload) + + data = &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + Value: model.NewScaledNumberType(16), + }, + }, + } + + payload.Data = data + + s.sut.loadControlLimitDataUpdate(payload) +} + +func (s *UCLPCServerSuite) Test_configurationDataUpdate() { + localDevice := s.service.LocalDevice() + localEntity := localDevice.EntityForType(model.EntityTypeTypeCEM) + + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.monitoredEntity, + } + s.sut.configurationDataUpdate(payload) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(1)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit), + }, + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(2)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + }, + }, + } + + lFeature := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + lFeature.SetData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData) + + s.sut.configurationDataUpdate(payload) + + data := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{}, + } + + payload.Data = data + + s.sut.configurationDataUpdate(payload) + + data = &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(1)), + Value: eebusutil.Ptr(model.DeviceConfigurationKeyValueValueType{}), + }, + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(2)), + Value: eebusutil.Ptr(model.DeviceConfigurationKeyValueValueType{}), + }, + }, + } + + payload.Data = data + + s.sut.configurationDataUpdate(payload) +} diff --git a/uclpcserver/public.go b/uclpcserver/public.go new file mode 100644 index 0000000..e95e07b --- /dev/null +++ b/uclpcserver/public.go @@ -0,0 +1,269 @@ +package uclpcserver + +import ( + "errors" + "time" + + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" +) + +// Scenario 1 + +// return the current loadcontrol limit data +// +// return values: +// - limit: load limit data +// +// possible errors: +// - ErrDataNotAvailable if no such limit is (yet) available +// - and others +func (e *UCLPCServer) ConsumptionLimit() (limit api.LoadLimit, resultErr error) { + limit = api.LoadLimit{ + Value: 0.0, + IsChangeable: false, + IsActive: false, + Duration: 0, + } + resultErr = eebusapi.ErrDataNotAvailable + + limidId, err := e.loadControlLimitId() + if err != nil { + return + } + + value := util.GetLocalLimitValueForLimitId(e.service, limidId) + if value.LimitId == nil || value.Value == nil { + return + } + + limit.Value = value.Value.GetValue() + limit.IsChangeable = (value.IsLimitChangeable != nil && *value.IsLimitChangeable) + limit.IsActive = (value.IsLimitActive != nil && *value.IsLimitActive) + if value.TimePeriod != nil && value.TimePeriod.EndTime != nil { + if duration, err := value.TimePeriod.EndTime.GetTimeDuration(); err == nil { + limit.Duration = duration + } + } + + return limit, nil +} + +// set the current loadcontrol limit data +func (e *UCLPCServer) SetConsumptionLimit(limit api.LoadLimit) (resultErr error) { + resultErr = eebusapi.ErrDataNotAvailable + + limidId, err := e.loadControlLimitId() + if err != nil { + return + } + + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + loadControl := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + if loadControl == nil { + return + } + + limitData := model.LoadControlLimitDataType{ + LimitId: eebusutil.Ptr(limidId), + IsLimitChangeable: eebusutil.Ptr(limit.IsChangeable), + IsLimitActive: eebusutil.Ptr(limit.IsActive), + Value: model.NewScaledNumberType(limit.Value), + } + if limit.Duration > 0 { + limitData.TimePeriod = &model.TimePeriodType{ + EndTime: model.NewAbsoluteOrRelativeTimeTypeFromDuration(limit.Duration), + } + } + // TODO: this overwrites LPP data as well + limits := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{limitData}, + } + + loadControl.SetData(model.FunctionTypeLoadControlLimitListData, limits) + + return nil +} + +// return the currently pending incoming consumption write limits +func (e *UCLPCServer) PendingConsumptionLimits() map[model.MsgCounterType]api.LoadLimit { + result := make(map[model.MsgCounterType]api.LoadLimit) + + limitId, err := e.loadControlLimitId() + if err != nil { + return result + } + + e.pendingMux.Lock() + defer e.pendingMux.Unlock() + + for key, msg := range e.pendingLimits { + data := msg.Cmd.LoadControlLimitListData + + // elements are only added to the map if all required fields exist + // therefor not check for these are needed here + + // find the item which contains the limit for this usecase + for _, item := range data.LoadControlLimitData { + if item.LimitId == nil || + limitId != *item.LimitId { + continue + } + + limit := api.LoadLimit{} + + if item.TimePeriod != nil { + if duration, err := item.TimePeriod.GetDuration(); err == nil { + limit.Duration = duration + } + } + + if item.IsLimitActive != nil { + limit.IsActive = *item.IsLimitActive + } + + if item.Value != nil { + limit.Value = item.Value.GetValue() + } + + result[key] = limit + } + } + + return result +} + +// accept or deny an incoming consumption write limit +// +// use PendingConsumptionLimits to get the list of currently pending requests +func (e *UCLPCServer) ApproveOrDenyConsumptionLimit(msgCounter model.MsgCounterType, approve bool, reason string) { + e.pendingMux.Lock() + defer e.pendingMux.Unlock() + + msg, ok := e.pendingLimits[msgCounter] + if !ok { + return + } + + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + f := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + + result := model.ErrorType{ + ErrorNumber: model.ErrorNumberType(0), + } + if !approve { + result.ErrorNumber = model.ErrorNumberType(7) + result.Description = eebusutil.Ptr(model.DescriptionType(reason)) + } + f.ApproveOrDenyWrite(msg, result) +} + +// Scenario 2 + +// return Failsafe limit for the consumed active (real) power of the +// Controllable System. This limit becomes activated in "init" state or "failsafe state". +func (e *UCLPCServer) FailsafeConsumptionActivePowerLimit() (limit float64, isChangeable bool, resultErr error) { + limit = 0 + isChangeable = false + resultErr = eebusapi.ErrDataNotAvailable + + keyName := model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit + keyData := util.GetLocalDeviceConfigurationKeyValueForKeyName(e.service, keyName) + if keyData.KeyId == nil || keyData.Value == nil || keyData.Value.ScaledNumber == nil { + return + } + + limit = keyData.Value.ScaledNumber.GetValue() + isChangeable = (keyData.IsValueChangeable != nil && *keyData.IsValueChangeable) + resultErr = nil + return +} + +// set Failsafe limit for the consumed active (real) power of the +// Controllable System. This limit becomes activated in "init" state or "failsafe state". +func (e *UCLPCServer) SetFailsafeConsumptionActivePowerLimit(value float64, changeable bool) error { + keyName := model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit + keyValue := model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(value), + } + + return util.SetLocalDeviceConfigurationKeyValue(e.service, keyName, changeable, keyValue) +} + +// return minimum time the Controllable System remains in "failsafe state" unless conditions +// specified in this Use Case permit leaving the "failsafe state" +func (e *UCLPCServer) FailsafeDurationMinimum() (duration time.Duration, isChangeable bool, resultErr error) { + duration = 0 + isChangeable = false + resultErr = eebusapi.ErrDataNotAvailable + + keyName := model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum + keyData := util.GetLocalDeviceConfigurationKeyValueForKeyName(e.service, keyName) + if keyData.KeyId == nil || keyData.Value == nil || keyData.Value.Duration == nil { + return + } + + durationValue, err := keyData.Value.Duration.GetTimeDuration() + if err != nil { + return + } + + duration = durationValue + isChangeable = (keyData.IsValueChangeable != nil && *keyData.IsValueChangeable) + resultErr = nil + return +} + +// set minimum time the Controllable System remains in "failsafe state" unless conditions +// specified in this Use Case permit leaving the "failsafe state" +// +// parameters: +// - duration: has to be >= 2h and <= 24h +// - changeable: boolean if the client service can change this value +func (e *UCLPCServer) SetFailsafeDurationMinimum(duration time.Duration, changeable bool) error { + if duration < time.Duration(time.Hour*2) || duration > time.Duration(time.Hour*24) { + return errors.New("duration outside of allowed range") + } + keyName := model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum + keyValue := model.DeviceConfigurationKeyValueValueType{ + Duration: model.NewDurationType(duration), + } + + return util.SetLocalDeviceConfigurationKeyValue(e.service, keyName, changeable, keyValue) +} + +// Scenario 4 + +// return nominal maximum active (real) power the Controllable System is +// allowed to consume due to the customer's contract. +func (e *UCLPCServer) ContractualConsumptionNominalMax() (value float64, resultErr error) { + value = 0 + resultErr = eebusapi.ErrDataNotAvailable + + charData := util.GetLocalElectricalConnectionCharacteristicForContextType( + e.service, + model.ElectricalConnectionCharacteristicContextTypeEntity, + model.ElectricalConnectionCharacteristicTypeTypeContractualConsumptionNominalMax, + ) + if charData.CharacteristicId == nil || charData.Value == nil { + return + } + + return charData.Value.GetValue(), nil +} + +// set nominal maximum active (real) power the Controllable System is +// allowed to consume due to the customer's contract. +func (e *UCLPCServer) SetContractualConsumptionNominalMax(value float64) error { + return util.SetLocalElectricalConnectionCharacteristicForContextType( + e.service, + model.ElectricalConnectionCharacteristicContextTypeEntity, + model.ElectricalConnectionCharacteristicTypeTypeContractualConsumptionNominalMax, + value, + ) +} diff --git a/uclpcserver/public_test.go b/uclpcserver/public_test.go new file mode 100644 index 0000000..54fa1d6 --- /dev/null +++ b/uclpcserver/public_test.go @@ -0,0 +1,116 @@ +package uclpcserver + +import ( + "time" + + "github.com/enbility/cemd/api" + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCLPCServerSuite) Test_ConsumptionLimit() { + limit, err := s.sut.ConsumptionLimit() + assert.Equal(s.T(), 0.0, limit.Value) + assert.NotNil(s.T(), err) + + newLimit := api.LoadLimit{ + Duration: time.Duration(time.Hour * 2), + IsActive: true, + IsChangeable: true, + Value: 16, + } + err = s.sut.SetConsumptionLimit(newLimit) + assert.Nil(s.T(), err) + + limit, err = s.sut.ConsumptionLimit() + assert.Equal(s.T(), 16.0, limit.Value) + assert.Nil(s.T(), err) +} + +func (s *UCLPCServerSuite) Test_PendingConsumptionLimits() { + data := s.sut.PendingConsumptionLimits() + assert.Equal(s.T(), 0, len(data)) + + msgCounter := model.MsgCounterType(500) + + msg := &spineapi.Message{ + RequestHeader: &model.HeaderType{ + MsgCounter: eebusutil.Ptr(msgCounter), + }, + Cmd: model.CmdType{ + LoadControlLimitListData: &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + IsLimitActive: eebusutil.Ptr(true), + Value: model.NewScaledNumberType(1000), + TimePeriod: model.NewTimePeriodTypeWithRelativeEndTime(time.Minute * 2), + }, + }, + }, + }, + DeviceRemote: s.remoteDevice, + EntityRemote: s.monitoredEntity, + } + + s.sut.loadControlWriteCB(msg) + + data = s.sut.PendingConsumptionLimits() + assert.Equal(s.T(), 1, len(data)) + + s.sut.ApproveOrDenyConsumptionLimit(model.MsgCounterType(499), true, "") + + s.sut.ApproveOrDenyConsumptionLimit(msgCounter, false, "leave me alone") +} + +func (s *UCLPCServerSuite) Test_Failsafe() { + limit, changeable, err := s.sut.FailsafeConsumptionActivePowerLimit() + assert.Equal(s.T(), 0.0, limit) + assert.Equal(s.T(), false, changeable) + assert.NotNil(s.T(), err) + + err = s.sut.SetFailsafeConsumptionActivePowerLimit(10, true) + assert.Nil(s.T(), err) + + limit, changeable, err = s.sut.FailsafeConsumptionActivePowerLimit() + assert.Equal(s.T(), 10.0, limit) + assert.Equal(s.T(), true, changeable) + assert.Nil(s.T(), err) + + // The actual tests of the functionality is located in the util package + duration, changeable, err := s.sut.FailsafeDurationMinimum() + assert.Equal(s.T(), time.Duration(0), duration) + assert.Equal(s.T(), false, changeable) + assert.NotNil(s.T(), err) + + err = s.sut.SetFailsafeDurationMinimum(time.Duration(time.Hour*1), true) + assert.NotNil(s.T(), err) + + err = s.sut.SetFailsafeDurationMinimum(time.Duration(time.Hour*2), true) + assert.Nil(s.T(), err) + + limit, changeable, err = s.sut.FailsafeConsumptionActivePowerLimit() + assert.Equal(s.T(), 10.0, limit) + assert.Equal(s.T(), true, changeable) + assert.Nil(s.T(), err) + + duration, changeable, err = s.sut.FailsafeDurationMinimum() + assert.Equal(s.T(), time.Duration(time.Hour*2), duration) + assert.Equal(s.T(), true, changeable) + assert.Nil(s.T(), err) +} + +func (s *UCLPCServerSuite) Test_ContractualConsumptionNominalMax() { + value, err := s.sut.ContractualConsumptionNominalMax() + assert.Equal(s.T(), 0.0, value) + assert.NotNil(s.T(), err) + + err = s.sut.SetContractualConsumptionNominalMax(10) + assert.Nil(s.T(), err) + + value, err = s.sut.ContractualConsumptionNominalMax() + assert.Equal(s.T(), 10.0, value) + assert.Nil(s.T(), err) +} diff --git a/uclpcserver/testhelper_test.go b/uclpcserver/testhelper_test.go new file mode 100644 index 0000000..2814c66 --- /dev/null +++ b/uclpcserver/testhelper_test.go @@ -0,0 +1,197 @@ +package uclpcserver + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/cemd/api" + eebusapi "github.com/enbility/eebus-go/api" + eebusmocks "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestLPCServerSuite(t *testing.T) { + suite.Run(t, new(UCLPCServerSuite)) +} + +type UCLPCServerSuite struct { + suite.Suite + + sut *UCLPCServer + + service eebusapi.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *mocks.EntityRemoteInterface + monitoredEntity spineapi.EntityRemoteInterface + loadControlFeature, + deviceDiagnosisFeature, + deviceConfigurationFeature spineapi.FeatureLocalInterface +} + +func (s *UCLPCServerSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { +} + +func (s *UCLPCServerSuite) BeforeTest(suiteName, testName string) { + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := eebusapi.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := eebusmocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := mocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = mocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := mocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + s.sut = NewUCLPC(s.service, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + localEntity := s.sut.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + s.loadControlFeature = localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + s.deviceDiagnosisFeature = localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + s.deviceConfigurationFeature = localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + + s.remoteDevice, s.monitoredEntity = setupDevices(s.service, s.T()) +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService eebusapi.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + localEntity := localDevice.EntityForType(model.EntityTypeTypeCEM) + + f := spine.NewFeatureLocal(1, localEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeLoadControlLimitDescriptionListData, true, false) + f.AddFunctionType(model.FunctionTypeLoadControlLimitListData, true, true) + localEntity.AddFeature(f) + f = spine.NewFeatureLocal(2, localEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeElectricalConnectionParameterDescriptionListData, true, false) + f.AddFunctionType(model.FunctionTypeElectricalConnectionPermittedValueSetListData, true, false) + f.AddFunctionType(model.FunctionTypeElectricalConnectionCharacteristicListData, true, true) + localEntity.AddFeature(f) + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + role model.RoleType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeLoadControl, + model.RoleTypeClient, + []model.FunctionType{}, + }, + {model.FeatureTypeTypeDeviceConfiguration, + model.RoleTypeClient, + []model.FunctionType{}, + }, + {model.FeatureTypeTypeDeviceDiagnosis, + model.RoleTypeClient, + []model.FunctionType{}, + }, + {model.FeatureTypeTypeDeviceDiagnosis, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeDeviceDiagnosisHeartbeatData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + model.RoleTypeClient, + []model.FunctionType{}, + }, + } + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: eebusutil.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + Feature: eebusutil.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: eebusutil.Ptr(feature.featureType), + Role: eebusutil.Ptr(feature.role), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeGridGuard), + }, + }, + }, + FeatureInformation: featureInformations, + } + + entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + remoteDevice.UpdateDevice(detailedData.DeviceInformation.Description) + + localDevice.AddRemoteDeviceForSki(remoteSki, remoteDevice) + + return remoteDevice, entities[0] +} diff --git a/uclpcserver/types.go b/uclpcserver/types.go new file mode 100644 index 0000000..f555694 --- /dev/null +++ b/uclpcserver/types.go @@ -0,0 +1,36 @@ +package uclpcserver + +import "github.com/enbility/cemd/api" + +const ( + // Load control obligation limit data update received + // + // Use `ConsumptionLimit` to get the current data + // + // Use Case LPC, Scenario 1 + DataUpdateLimit api.EventType = "DataUpdateLimit" + + // An incoming load control obligation limit needs to be approved or denied + // + // Use `PendingConsumptionLimits` to get the currently pending write approval requests + // and invoke `ApproveOrDenyConsumptionLimit` for each + // + // Use Case LPC, Scenario 1 + WriteApprovalRequired api.EventType = "WriteApprovalRequired" + + // Failsafe limit for the consumed active (real) power of the + // Controllable System data update received + // + // Use `FailsafeConsumptionActivePowerLimit` to get the current data + // + // Use Case LPC, Scenario 2 + DataUpdateFailsafeConsumptionActivePowerLimit api.EventType = "DataUpdateFailsafeConsumptionActivePowerLimit" + + // Minimum time the Controllable System remains in "failsafe state" unless conditions + // specified in this Use Case permit leaving the "failsafe state" data update received + // + // Use `FailsafeDurationMinimum` to get the current data + // + // Use Case LPC, Scenario 2 + DataUpdateFailsafeDurationMinimum api.EventType = "DataUpdateFailsafeDurationMinimum" +) diff --git a/uclpcserver/uclpc.go b/uclpcserver/uclpc.go new file mode 100644 index 0000000..f0bc76a --- /dev/null +++ b/uclpcserver/uclpc.go @@ -0,0 +1,304 @@ +package uclpcserver + +import ( + "errors" + "sync" + + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +type UCLPCServer struct { + service eebusapi.ServiceInterface + + eventCB api.EntityEventCallback + + validEntityTypes []model.EntityTypeType + + pendingMux sync.Mutex + pendingLimits map[model.MsgCounterType]*spineapi.Message + + heartbeatKeoWorkaround bool // required because KEO Stack uses multiple identical entities for the same functionality, and it is not clear which to use +} + +var _ UCLPCServerInterface = (*UCLPCServer)(nil) + +func NewUCLPC(service eebusapi.ServiceInterface, eventCB api.EntityEventCallback) *UCLPCServer { + uc := &UCLPCServer{ + service: service, + eventCB: eventCB, + pendingLimits: make(map[model.MsgCounterType]*spineapi.Message), + } + + uc.validEntityTypes = []model.EntityTypeType{ + model.EntityTypeTypeGridGuard, + model.EntityTypeTypeCEM, // KEO uses this entity type for an SMGW whysoever + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (c *UCLPCServer) UseCaseName() model.UseCaseNameType { + return model.UseCaseNameTypeLimitationOfPowerConsumption +} + +func (e *UCLPCServer) loadControlLimitId() (limitid model.LoadControlLimitIdType, err error) { + limitid = model.LoadControlLimitIdType(0) + err = errors.New("not found") + + descriptions := util.GetLocalLimitDescriptionsForTypeCategoryDirectionScope( + e.service, + model.LoadControlLimitTypeTypeSignDependentAbsValueLimit, + model.LoadControlCategoryTypeObligation, + model.EnergyDirectionTypeConsume, + model.ScopeTypeTypeActivePowerLimit, + ) + if len(descriptions) != 1 || descriptions[0].LimitId == nil { + return + } + description := descriptions[0] + + if description.LimitId == nil { + return + } + + return *description.LimitId, nil +} + +// callback invoked on incoming write messages to this +// loadcontrol server feature. +// the implementation only considers write messages for this use case and +// approves all others +func (e *UCLPCServer) loadControlWriteCB(msg *spineapi.Message) { + e.pendingMux.Lock() + defer e.pendingMux.Unlock() + + if msg.RequestHeader == nil || msg.RequestHeader.MsgCounter == nil || + msg.Cmd.LoadControlLimitListData == nil { + return + } + + limitId, err := e.loadControlLimitId() + if err != nil { + return + } + + data := msg.Cmd.LoadControlLimitListData + + // we assume there is always only one limit + if data == nil || data.LoadControlLimitData == nil || + len(data.LoadControlLimitData) == 0 { + return + } + + // check if there is a matching limitId in the data + for _, item := range data.LoadControlLimitData { + if item.LimitId == nil || + limitId != *item.LimitId { + continue + } + + if _, ok := e.pendingLimits[*msg.RequestHeader.MsgCounter]; !ok { + e.pendingLimits[*msg.RequestHeader.MsgCounter] = msg + e.eventCB(msg.DeviceRemote.Ski(), msg.DeviceRemote, msg.EntityRemote, WriteApprovalRequired) + return + } + } + + // approve, because this is no request for this usecase + go e.ApproveOrDenyConsumptionLimit(*msg.RequestHeader.MsgCounter, true, "") +} + +func (e *UCLPCServer) AddFeatures() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + // client features + _ = localEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeClient) + + // server features + f := localEntity.GetOrAddFeature(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeLoadControlLimitDescriptionListData, true, false) + f.AddFunctionType(model.FunctionTypeLoadControlLimitListData, true, true) + _ = f.AddWriteApprovalCallback(e.loadControlWriteCB) + + var limitId model.LoadControlLimitIdType = 0 + // get the highest limitId + desc, err := spine.LocalFeatureDataCopyOfType[*model.LoadControlLimitDescriptionListDataType]( + f, model.FunctionTypeLoadControlLimitDescriptionListData) + if err == nil && desc.LoadControlLimitDescriptionData != nil { + for _, desc := range desc.LoadControlLimitDescriptionData { + if desc.LimitId != nil && *desc.LimitId >= limitId { + limitId++ + } + } + } + + if desc == nil || len(desc.LoadControlLimitDescriptionData) == 0 { + desc = &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{}, + } + } + + newLimitDesc := model.LoadControlLimitDescriptionDataType{ + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(limitId)), + LimitType: eebusutil.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitCategory: eebusutil.Ptr(model.LoadControlCategoryTypeObligation), + LimitDirection: eebusutil.Ptr(model.EnergyDirectionTypeConsume), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), // This is a fake Measurement ID, as there is no Electrical Connection server defined, it can't provide any meaningful. But KEO requires this to be set :( + Unit: eebusutil.Ptr(model.UnitOfMeasurementTypeW), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeActivePowerLimit), + } + desc.LoadControlLimitDescriptionData = append(desc.LoadControlLimitDescriptionData, newLimitDesc) + f.SetData(model.FunctionTypeLoadControlLimitDescriptionListData, desc) + + f = localEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, true, false) + f.AddFunctionType(model.FunctionTypeDeviceConfigurationKeyValueListData, true, true) + + var configId model.DeviceConfigurationKeyIdType = 0 + // get the highest keyId + deviceConfigDesc, err := spine.LocalFeatureDataCopyOfType[*model.DeviceConfigurationKeyValueDescriptionListDataType]( + f, model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData) + if err == nil && deviceConfigDesc.DeviceConfigurationKeyValueDescriptionData != nil { + for _, desc := range deviceConfigDesc.DeviceConfigurationKeyValueDescriptionData { + if desc.KeyId != nil && *desc.KeyId >= configId { + configId++ + } + } + } + + if err != nil || deviceConfigDesc == nil || len(deviceConfigDesc.DeviceConfigurationKeyValueDescriptionData) == 0 { + deviceConfigDesc = &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{}, + } + } + + newConfigs := []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit), + ValueType: eebusutil.Ptr(model.DeviceConfigurationKeyValueTypeTypeScaledNumber), + Unit: eebusutil.Ptr(model.UnitOfMeasurementTypeW), + }, + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId + 1)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + ValueType: eebusutil.Ptr(model.DeviceConfigurationKeyValueTypeTypeDuration), + }, + } + deviceConfigDesc.DeviceConfigurationKeyValueDescriptionData = append(deviceConfigDesc.DeviceConfigurationKeyValueDescriptionData, newConfigs...) + f.SetData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, deviceConfigDesc) + + configData, err := spine.LocalFeatureDataCopyOfType[*model.DeviceConfigurationKeyValueListDataType](f, model.FunctionTypeDeviceConfigurationKeyValueListData) + if err != nil || configData == nil || len(configData.DeviceConfigurationKeyValueData) == 0 { + configData = &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{}, + } + } + + newConfigData := []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId)), + IsValueChangeable: eebusutil.Ptr(true), + }, + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId + 1)), + IsValueChangeable: eebusutil.Ptr(true), + }, + } + + configData.DeviceConfigurationKeyValueData = append(configData.DeviceConfigurationKeyValueData, newConfigData...) + f.SetData(model.FunctionTypeDeviceConfigurationKeyValueListData, configData) + + f = localEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeDeviceDiagnosisHeartbeatData, true, false) + + f = localEntity.GetOrAddFeature(model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeElectricalConnectionCharacteristicListData, true, false) + + var elCharId model.ElectricalConnectionCharacteristicIdType = 0 + // get the highest CharacteristicId + elCharData, err := spine.LocalFeatureDataCopyOfType[*model.ElectricalConnectionCharacteristicListDataType]( + f, model.FunctionTypeElectricalConnectionCharacteristicListData) + if err == nil && elCharData.ElectricalConnectionCharacteristicData != nil { + for _, desc := range elCharData.ElectricalConnectionCharacteristicData { + if desc.CharacteristicId != nil && *desc.CharacteristicId >= elCharId { + elCharId++ + } + } + } + + if err != nil || configData == nil || len(configData.DeviceConfigurationKeyValueData) == 0 { + elCharData = &model.ElectricalConnectionCharacteristicListDataType{ + ElectricalConnectionCharacteristicData: []model.ElectricalConnectionCharacteristicDataType{}, + } + } + + // ElectricalConnectionId and ParameterId should be identical to the ones used + // in a MPC Server role implementation, which is not done here (yet) + newCharData := model.ElectricalConnectionCharacteristicDataType{ + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(0)), + CharacteristicId: eebusutil.Ptr(elCharId), + CharacteristicContext: eebusutil.Ptr(model.ElectricalConnectionCharacteristicContextTypeEntity), + CharacteristicType: eebusutil.Ptr(model.ElectricalConnectionCharacteristicTypeTypeContractualConsumptionNominalMax), + Unit: eebusutil.Ptr(model.UnitOfMeasurementTypeW), + } + elCharData.ElectricalConnectionCharacteristicData = append(elCharData.ElectricalConnectionCharacteristicData, newCharData) + f.SetData(model.FunctionTypeElectricalConnectionCharacteristicListData, elCharData) +} + +func (e *UCLPCServer) AddUseCase() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.AddUseCaseSupport( + model.UseCaseActorTypeControllableSystem, + e.UseCaseName(), + model.SpecificationVersionType("1.0.0"), + "release", + true, + []model.UseCaseScenarioSupportType{1, 2, 3, 4}) +} + +func (e *UCLPCServer) UpdateUseCaseAvailability(available bool) { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.SetUseCaseAvailability(model.UseCaseActorTypeControllableSystem, e.UseCaseName(), available) +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *UCLPCServer) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return false, api.ErrNoCompatibleEntity + } + + // check if the usecase and mandatory scenarios are supported and + // if the required server features are available + if !entity.Device().VerifyUseCaseScenariosAndFeaturesSupport( + model.UseCaseActorTypeEnergyGuard, + e.UseCaseName(), + []model.UseCaseScenarioSupportType{1, 2, 3, 4}, + []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceDiagnosis, + }, + ) { + return false, nil + } + + if _, err := util.DeviceDiagnosis(e.service, entity); err != nil { + return false, eebusapi.ErrFunctionNotSupported + } + + return true, nil +} diff --git a/uclpcserver/uclpc_test.go b/uclpcserver/uclpc_test.go new file mode 100644 index 0000000..ee3ee5f --- /dev/null +++ b/uclpcserver/uclpc_test.go @@ -0,0 +1,100 @@ +package uclpcserver + +import ( + "time" + + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCLPCServerSuite) Test_loadControlWriteCB() { + msg := &spineapi.Message{} + + s.sut.loadControlWriteCB(msg) + + msg = &spineapi.Message{ + RequestHeader: &model.HeaderType{ + MsgCounter: eebusutil.Ptr(model.MsgCounterType(500)), + }, + Cmd: model.CmdType{ + LoadControlLimitListData: &model.LoadControlLimitListDataType{}, + }, + DeviceRemote: s.remoteDevice, + EntityRemote: s.monitoredEntity, + } + + s.sut.loadControlWriteCB(msg) + + msg.Cmd = model.CmdType{ + LoadControlLimitListData: &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{}, + }, + } + + s.sut.loadControlWriteCB(msg) + + msg.Cmd = model.CmdType{ + LoadControlLimitListData: &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + {}, + }, + }, + } + + s.sut.loadControlWriteCB(msg) + + msg.Cmd = model.CmdType{ + LoadControlLimitListData: &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + IsLimitActive: eebusutil.Ptr(true), + Value: model.NewScaledNumberType(1000), + TimePeriod: model.NewTimePeriodTypeWithRelativeEndTime(time.Minute * 2), + }, + }, + }, + } + + s.sut.loadControlWriteCB(msg) +} + +func (s *UCLPCServerSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *UCLPCServerSuite) Test_IsUseCaseSupported() { + data, err := s.sut.IsUseCaseSupported(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + data, err = s.sut.IsUseCaseSupported(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: eebusutil.Ptr(model.UseCaseActorTypeEnergyGuard), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: eebusutil.Ptr(model.UseCaseNameTypeLimitationOfPowerConsumption), + UseCaseAvailable: eebusutil.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1, 2, 3, 4}, + }, + }, + }, + }, + } + + nodemgmtEntity := s.remoteDevice.Entity([]model.AddressEntityType{0}) + nodeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(nodemgmtEntity, model.FeatureTypeTypeNodeManagement, model.RoleTypeSpecial) + fErr := nodeFeature.UpdateData(model.FunctionTypeNodeManagementUseCaseData, ucData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/uclpp/api.go b/uclpp/api.go new file mode 100644 index 0000000..53c64e9 --- /dev/null +++ b/uclpp/api.go @@ -0,0 +1,87 @@ +package uclpp + +import ( + "time" + + "github.com/enbility/cemd/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +//go:generate mockery + +// interface for the Limitation of Power Production UseCase +type UCLPPInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // return the current production limit data + // + // parameters: + // - entity: the entity of the e.g. EVSE + // + // return values: + // - limit: load limit data + // + // possible errors: + // - ErrDataNotAvailable if no such limit is (yet) available + // - and others + ProductionLimit(entity spineapi.EntityRemoteInterface) (limit api.LoadLimit, resultErr error) + + // send new LoadControlLimits + // + // parameters: + // - entity: the entity of the e.g. EVSE + // - limit: load limit data + WriteProductionLimit(entity spineapi.EntityRemoteInterface, limit api.LoadLimit) (*model.MsgCounterType, error) + + // Scenario 2 + + // return Failsafe limit for the produced active (real) power of the + // Controllable System. This limit becomes activated in "init" state or "failsafe state". + // + // parameters: + // - entity: the entity of the e.g. EVSE + // + // return values: + // - positive values are used for production + FailsafeProductionActivePowerLimit(entity spineapi.EntityRemoteInterface) (float64, error) + + // send new Failsafe Production Active Power Limit + // + // parameters: + // - entity: the entity of the e.g. EVSE + // - value: the new limit in W + WriteFailsafeProductionActivePowerLimit(entity spineapi.EntityRemoteInterface, value float64) (*model.MsgCounterType, error) + + // return minimum time the Controllable System remains in "failsafe state" unless conditions + // specified in this Use Case permit leaving the "failsafe state" + // + // parameters: + // - entity: the entity of the e.g. EVSE + // + // return values: + // - negative values are used for production + FailsafeDurationMinimum(entity spineapi.EntityRemoteInterface) (time.Duration, error) + + // send new Failsafe Duration Minimum + // + // parameters: + // - entity: the entity of the e.g. EVSE + // - duration: the duration, between 2h and 24h + WriteFailsafeDurationMinimum(entity spineapi.EntityRemoteInterface, duration time.Duration) (*model.MsgCounterType, error) + + // Scenario 3 + + // this is automatically covered by the SPINE implementation + + // Scenario 4 + + // return nominal maximum active (real) power the Controllable System is + // able to produce according to the device label or data sheet. + // + // parameters: + // - entity: the entity of the e.g. EVSE + PowerProductionNominalMax(entity spineapi.EntityRemoteInterface) (float64, error) +} diff --git a/uclpp/events.go b/uclpp/events.go new file mode 100644 index 0000000..61ad08b --- /dev/null +++ b/uclpp/events.go @@ -0,0 +1,99 @@ +package uclpp + +import ( + "github.com/enbility/cemd/util" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// handle SPINE events +func (e *UCLPP) HandleEvent(payload spineapi.EventPayload) { + if !util.IsCompatibleEntity(payload.Entity, e.validEntityTypes) { + return + } + + if util.IsEntityConnected(payload) { + e.connected(payload.Entity) + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + + switch payload.Data.(type) { + case *model.LoadControlLimitDescriptionListDataType: + e.loadControlLimitDescriptionDataUpdate(payload.Entity) + case *model.LoadControlLimitListDataType: + e.loadControlLimitDataUpdate(payload) + case *model.DeviceConfigurationKeyValueDescriptionListDataType: + e.configurationDescriptionDataUpdate(payload.Entity) + case *model.DeviceConfigurationKeyValueListDataType: + e.configurationDataUpdate(payload) + } +} + +// the remote entity was connected +func (e *UCLPP) connected(entity spineapi.EntityRemoteInterface) { + // initialise features, e.g. subscriptions, descriptions + if loadControl, err := util.LoadControl(e.service, entity); err == nil { + if _, err := loadControl.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + // get descriptions + if _, err := loadControl.RequestLimitDescriptions(); err != nil { + logging.Log().Debug(err) + } + } + + if localDeviceDiag, err := util.DeviceDiagnosis(e.service, entity); err == nil { + if _, err := localDeviceDiag.Subscribe(); err != nil { + logging.Log().Debug(err) + } + } +} + +// the load control limit description data was updated +func (e *UCLPP) loadControlLimitDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if loadControl, err := util.LoadControl(e.service, entity); err == nil { + // get values + if _, err := loadControl.RequestLimitValues(); err != nil { + logging.Log().Debug(err) + } + } +} + +// the load control limit data was updated +func (e *UCLPP) loadControlLimitDataUpdate(payload spineapi.EventPayload) { + if util.LoadControlLimitsCheckPayloadDataForTypeCategoryDirectionScope( + false, e.service, payload, + model.LoadControlLimitTypeTypeSignDependentAbsValueLimit, + model.LoadControlCategoryTypeObligation, + model.EnergyDirectionTypeProduce, + model.ScopeTypeTypeActivePowerLimit) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateLimit) + } +} + +// the configuration key description data was updated +func (e *UCLPP) configurationDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if deviceConfiguration, err := util.DeviceConfiguration(e.service, entity); err == nil { + // key value descriptions received, now get the data + if _, err := deviceConfiguration.RequestKeyValues(); err != nil { + logging.Log().Error("Error getting configuration key values:", err) + } + } +} + +// the configuration key data was updated +func (e *UCLPP) configurationDataUpdate(payload spineapi.EventPayload) { + if util.DeviceConfigurationCheckDataPayloadForKeyName(false, e.service, payload, model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateFailsafeProductionActivePowerLimit) + } + if util.DeviceConfigurationCheckDataPayloadForKeyName(false, e.service, payload, model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateFailsafeDurationMinimum) + } +} diff --git a/uclpp/events_test.go b/uclpp/events_test.go new file mode 100644 index 0000000..8790725 --- /dev/null +++ b/uclpp/events_test.go @@ -0,0 +1,147 @@ +package uclpp + +import ( + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCLPPSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.monitoredEntity + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeEntityChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.Data = eebusutil.Ptr(model.LoadControlLimitDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.LoadControlLimitListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.DeviceConfigurationKeyValueDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.DeviceConfigurationKeyValueListDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *UCLPPSuite) Test_Failures() { + s.sut.connected(s.mockRemoteEntity) + + s.sut.configurationDescriptionDataUpdate(s.mockRemoteEntity) +} + +func (s *UCLPPSuite) Test_loadControlLimitDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.monitoredEntity, + } + s.sut.loadControlLimitDataUpdate(payload) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + LimitType: eebusutil.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitCategory: eebusutil.Ptr(model.LoadControlCategoryTypeObligation), + LimitDirection: eebusutil.Ptr(model.EnergyDirectionTypeProduce), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeActivePowerLimit), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeLoadControlLimitDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.loadControlLimitDataUpdate(payload) + + data := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{}, + } + + payload.Data = data + + s.sut.loadControlLimitDataUpdate(payload) + + data = &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + Value: model.NewScaledNumberType(16), + }, + }, + } + + payload.Data = data + + s.sut.loadControlLimitDataUpdate(payload) +} + +func (s *UCLPPSuite) Test_configurationDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.monitoredEntity, + } + s.sut.configurationDataUpdate(payload) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(1)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit), + }, + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(2)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.configurationDataUpdate(payload) + + data := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{}, + } + + payload.Data = data + + s.sut.configurationDataUpdate(payload) + + data = &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(1)), + Value: &model.DeviceConfigurationKeyValueValueType{}, + }, + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(2)), + Value: &model.DeviceConfigurationKeyValueValueType{}, + }, + }, + } + + payload.Data = data + + s.sut.configurationDataUpdate(payload) +} diff --git a/uclpp/public.go b/uclpp/public.go new file mode 100644 index 0000000..49e43c1 --- /dev/null +++ b/uclpp/public.go @@ -0,0 +1,304 @@ +package uclpp + +import ( + "errors" + "time" + + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// Scenario 1 + +// return the current loadcontrol limit data +// +// parameters: +// - entity: the entity of the e.g. EVSE +// +// return values: +// - limit: load limit data +// +// possible errors: +// - ErrDataNotAvailable if no such limit is (yet) available +// - and others +func (e *UCLPP) ProductionLimit(entity spineapi.EntityRemoteInterface) ( + limit api.LoadLimit, resultErr error) { + limit = api.LoadLimit{ + Value: 0.0, + IsChangeable: false, + IsActive: false, + } + + resultErr = api.ErrNoCompatibleEntity + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return + } + + resultErr = eebusapi.ErrDataNotAvailable + loadControl, err := util.LoadControl(e.service, entity) + if err != nil || loadControl == nil { + return + } + + limitDescriptions, err := loadControl.GetLimitDescriptionsForTypeCategoryDirectionScope( + model.LoadControlLimitTypeTypeSignDependentAbsValueLimit, + model.LoadControlCategoryTypeObligation, + model.EnergyDirectionTypeProduce, + model.ScopeTypeTypeActivePowerLimit) + if err != nil || len(limitDescriptions) != 1 { + return + } + + value, err := loadControl.GetLimitValueForLimitId(*limitDescriptions[0].LimitId) + if err != nil || value.Value == nil { + return + } + + limit.Value = value.Value.GetValue() + limit.IsChangeable = (value.IsLimitChangeable != nil && *value.IsLimitChangeable) + limit.IsActive = (value.IsLimitActive != nil && *value.IsLimitActive) + if value.TimePeriod != nil && value.TimePeriod.EndTime != nil { + if duration, err := value.TimePeriod.EndTime.GetTimeDuration(); err == nil { + limit.Duration = duration + } + } + + resultErr = nil + + return +} + +// send new LoadControlLimits +// +// parameters: +// - entity: the entity of the e.g. EVSE +// - limit: load limit data +func (e *UCLPP) WriteProductionLimit( + entity spineapi.EntityRemoteInterface, + limit api.LoadLimit) (*model.MsgCounterType, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return nil, api.ErrNoCompatibleEntity + } + + loadControl, err := util.LoadControl(e.service, entity) + if err != nil { + return nil, api.ErrNoCompatibleEntity + } + + var limitData []model.LoadControlLimitDataType + + limitDescriptions, err := loadControl.GetLimitDescriptionsForTypeCategoryDirectionScope( + model.LoadControlLimitTypeTypeSignDependentAbsValueLimit, + model.LoadControlCategoryTypeObligation, + model.EnergyDirectionTypeProduce, + model.ScopeTypeTypeActivePowerLimit, + ) + if err != nil || + len(limitDescriptions) != 1 || + limitDescriptions[0].LimitId == nil { + return nil, eebusapi.ErrMetadataNotAvailable + } + + limitDesc := limitDescriptions[0] + + if _, err := loadControl.GetLimitValueForLimitId(*limitDesc.LimitId); err != nil { + return nil, eebusapi.ErrDataNotAvailable + } + + currentLimits, err := loadControl.GetLimitValues() + if err != nil { + return nil, eebusapi.ErrDataNotAvailable + } + + for index, item := range currentLimits { + if item.LimitId == nil || + *item.LimitId != *limitDesc.LimitId { + continue + } + + // EEBus_UC_TS_LimitationOfPowerProduction V1.0.0 3.2.2.2.2.2 + // If set to "true", the timePeriod, value and isLimitActive Elements SHALL be writeable by a client. + if item.IsLimitChangeable != nil && !*item.IsLimitChangeable { + return nil, eebusapi.ErrNotSupported + } + + newLimit := model.LoadControlLimitDataType{ + LimitId: limitDesc.LimitId, + IsLimitActive: eebusutil.Ptr(limit.IsActive), + Value: model.NewScaledNumberType(limit.Value), + } + if limit.Duration > 0 { + newLimit.TimePeriod = &model.TimePeriodType{ + EndTime: model.NewAbsoluteOrRelativeTimeTypeFromDuration(limit.Duration), + } + } + + currentLimits[index] = newLimit + break + } + + msgCounter, err := loadControl.WriteLimitValues(limitData) + + return msgCounter, err +} + +// Scenario 2 + +// return Failsafe limit for the produced active (real) power of the +// Controllable System. This limit becomes activated in "init" state or "failsafe state". +func (e *UCLPP) FailsafeProductionActivePowerLimit(entity spineapi.EntityRemoteInterface) (float64, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + + keyname := model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit + + deviceConfiguration, err := util.DeviceConfiguration(e.service, entity) + if err != nil || deviceConfiguration == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + data, err := deviceConfiguration.GetKeyValueForKeyName(keyname, model.DeviceConfigurationKeyValueTypeTypeScaledNumber) + if err != nil || data == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + value, ok := data.(*model.ScaledNumberType) + if !ok || value == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + return value.GetValue(), nil +} + +// send new Failsafe Production Active Power Limit +// +// parameters: +// - entity: the entity of the e.g. EVSE +// - value: the new limit in W +func (e *UCLPP) WriteFailsafeProductionActivePowerLimit(entity spineapi.EntityRemoteInterface, value float64) (*model.MsgCounterType, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return nil, api.ErrNoCompatibleEntity + } + + keyname := model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit + + deviceConfiguration, err := util.DeviceConfiguration(e.service, entity) + if err != nil || deviceConfiguration == nil { + return nil, eebusapi.ErrDataNotAvailable + } + + data, err := deviceConfiguration.GetDescriptionForKeyName(keyname) + if err != nil || data == nil || data.KeyId == nil { + return nil, eebusapi.ErrDataNotAvailable + } + + keyData := []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: data.KeyId, + Value: &model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(value), + }, + }, + } + + msgCounter, err := deviceConfiguration.WriteKeyValues(keyData) + + return msgCounter, err +} + +// return minimum time the Controllable System remains in "failsafe state" unless conditions +// specified in this Use Case permit leaving the "failsafe state" +func (e *UCLPP) FailsafeDurationMinimum(entity spineapi.EntityRemoteInterface) (time.Duration, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + + keyname := model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum + + deviceConfiguration, err := util.DeviceConfiguration(e.service, entity) + if err != nil || deviceConfiguration == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + data, err := deviceConfiguration.GetKeyValueForKeyName(keyname, model.DeviceConfigurationKeyValueTypeTypeDuration) + if err != nil || data == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + value, ok := data.(*model.DurationType) + if !ok || value == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + return value.GetTimeDuration() +} + +// send new Failsafe Duration Minimum +// +// parameters: +// - entity: the entity of the e.g. EVSE +// - duration: the duration, between 2h and 24h +func (e *UCLPP) WriteFailsafeDurationMinimum(entity spineapi.EntityRemoteInterface, duration time.Duration) (*model.MsgCounterType, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return nil, api.ErrNoCompatibleEntity + } + + if duration < time.Duration(time.Hour*2) || duration > time.Duration(time.Hour*24) { + return nil, errors.New("duration outside of allowed range") + } + + keyname := model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum + + deviceConfiguration, err := util.DeviceConfiguration(e.service, entity) + if err != nil || deviceConfiguration == nil { + return nil, eebusapi.ErrDataNotAvailable + } + + data, err := deviceConfiguration.GetDescriptionForKeyName(keyname) + if err != nil || data == nil || data.KeyId == nil { + return nil, eebusapi.ErrDataNotAvailable + } + + keyData := []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: data.KeyId, + Value: &model.DeviceConfigurationKeyValueValueType{ + Duration: model.NewDurationType(duration), + }, + }, + } + + msgCounter, err := deviceConfiguration.WriteKeyValues(keyData) + + return msgCounter, err +} + +// Scenario 4 + +// return nominal maximum active (real) power the Controllable System is +// able to produce according to the device label or data sheet. +func (e *UCLPP) PowerProductionNominalMax(entity spineapi.EntityRemoteInterface) (float64, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + + electricalConnection, err := util.ElectricalConnection(e.service, entity) + if err != nil || electricalConnection == nil { + return 0, err + } + + data, err := electricalConnection.GetCharacteristicForContextType( + model.ElectricalConnectionCharacteristicContextTypeEntity, + model.ElectricalConnectionCharacteristicTypeTypePowerProductionNominalMax, + ) + if err != nil || data.Value == nil { + return 0, err + } + + return data.Value.GetValue(), nil +} diff --git a/uclpp/public_test.go b/uclpp/public_test.go new file mode 100644 index 0000000..9b31d19 --- /dev/null +++ b/uclpp/public_test.go @@ -0,0 +1,355 @@ +package uclpp + +import ( + "time" + + "github.com/enbility/cemd/api" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCLPPSuite) Test_LoadControlLimit() { + data, err := s.sut.ProductionLimit(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data.Value) + assert.Equal(s.T(), false, data.IsChangeable) + assert.Equal(s.T(), false, data.IsActive) + + data, err = s.sut.ProductionLimit(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data.Value) + assert.Equal(s.T(), false, data.IsChangeable) + assert.Equal(s.T(), false, data.IsActive) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + LimitCategory: eebusutil.Ptr(model.LoadControlCategoryTypeObligation), + LimitType: eebusutil.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitDirection: eebusutil.Ptr(model.EnergyDirectionTypeProduce), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeActivePowerLimit), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeLoadControlLimitDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.ProductionLimit(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data.Value) + assert.Equal(s.T(), false, data.IsChangeable) + assert.Equal(s.T(), false, data.IsActive) + + limitData := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + IsLimitChangeable: eebusutil.Ptr(true), + IsLimitActive: eebusutil.Ptr(false), + Value: model.NewScaledNumberType(6000), + TimePeriod: &model.TimePeriodType{ + EndTime: model.NewAbsoluteOrRelativeTimeType("PT2H"), + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeLoadControlLimitListData, limitData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.ProductionLimit(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 6000.0, data.Value) + assert.Equal(s.T(), true, data.IsChangeable) + assert.Equal(s.T(), false, data.IsActive) +} + +func (s *UCLPPSuite) Test_WriteLoadControlLimit() { + limit := api.LoadLimit{ + Value: 6000, + IsActive: true, + Duration: 0, + } + _, err := s.sut.WriteProductionLimit(s.mockRemoteEntity, limit) + assert.NotNil(s.T(), err) + + _, err = s.sut.WriteProductionLimit(s.monitoredEntity, limit) + assert.NotNil(s.T(), err) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + LimitCategory: eebusutil.Ptr(model.LoadControlCategoryTypeObligation), + LimitType: eebusutil.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitDirection: eebusutil.Ptr(model.EnergyDirectionTypeProduce), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeActivePowerLimit), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeLoadControlLimitDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.WriteProductionLimit(s.monitoredEntity, limit) + assert.NotNil(s.T(), err) + + limitData := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + IsLimitChangeable: eebusutil.Ptr(true), + IsLimitActive: eebusutil.Ptr(false), + Value: model.NewScaledNumberType(6000), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeLoadControlLimitListData, limitData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.WriteProductionLimit(s.monitoredEntity, limit) + assert.NotNil(s.T(), err) + + limit.Duration = time.Duration(time.Hour * 2) + _, err = s.sut.WriteProductionLimit(s.monitoredEntity, limit) + assert.NotNil(s.T(), err) +} + +func (s *UCLPPSuite) Test_FailsafeProductionActivePowerLimit() { + data, err := s.sut.FailsafeProductionActivePowerLimit(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.FailsafeProductionActivePowerLimit(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.FailsafeProductionActivePowerLimit(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{}, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.FailsafeProductionActivePowerLimit(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + keyData = &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(4000), + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.FailsafeProductionActivePowerLimit(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 4000.0, data) +} + +func (s *UCLPPSuite) Test_WriteFailsafeProductionActivePowerLimit() { + _, err := s.sut.WriteFailsafeProductionActivePowerLimit(s.mockRemoteEntity, 6000) + assert.NotNil(s.T(), err) + + _, err = s.sut.WriteFailsafeProductionActivePowerLimit(s.monitoredEntity, 6000) + assert.NotNil(s.T(), err) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.WriteFailsafeProductionActivePowerLimit(s.monitoredEntity, 6000) + assert.Nil(s.T(), err) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{}, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.WriteFailsafeProductionActivePowerLimit(s.monitoredEntity, 6000) + assert.Nil(s.T(), err) +} + +func (s *UCLPPSuite) Test_FailsafeDurationMinimum() { + data, err := s.sut.FailsafeDurationMinimum(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), time.Duration(0), data) + + data, err = s.sut.FailsafeDurationMinimum(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), time.Duration(0), data) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.FailsafeDurationMinimum(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), time.Duration(0), data) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{}, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.FailsafeDurationMinimum(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), time.Duration(0), data) + + keyData = &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + Duration: model.NewDurationType(time.Hour * 2), + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.FailsafeDurationMinimum(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), time.Duration(time.Hour*2), data) +} + +func (s *UCLPPSuite) Test_WriteFailsafeDurationMinimum() { + _, err := s.sut.WriteFailsafeDurationMinimum(s.mockRemoteEntity, time.Duration(time.Hour*2)) + assert.NotNil(s.T(), err) + + _, err = s.sut.WriteFailsafeDurationMinimum(s.monitoredEntity, time.Duration(time.Hour*2)) + assert.NotNil(s.T(), err) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.WriteFailsafeDurationMinimum(s.monitoredEntity, time.Duration(time.Hour*2)) + assert.Nil(s.T(), err) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{}, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.WriteFailsafeDurationMinimum(s.monitoredEntity, time.Duration(time.Hour*2)) + assert.Nil(s.T(), err) + + _, err = s.sut.WriteFailsafeDurationMinimum(s.monitoredEntity, time.Duration(time.Hour*1)) + assert.NotNil(s.T(), err) +} + +func (s *UCLPPSuite) Test_PowerProductionNominalMax() { + data, err := s.sut.PowerProductionNominalMax(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.PowerProductionNominalMax(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + charData := &model.ElectricalConnectionCharacteristicListDataType{ + ElectricalConnectionCharacteristicData: []model.ElectricalConnectionCharacteristicDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + CharacteristicId: eebusutil.Ptr(model.ElectricalConnectionCharacteristicIdType(0)), + CharacteristicContext: eebusutil.Ptr(model.ElectricalConnectionCharacteristicContextTypeEntity), + CharacteristicType: eebusutil.Ptr(model.ElectricalConnectionCharacteristicTypeTypePowerProductionNominalMax), + Value: model.NewScaledNumberType(8000), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionCharacteristicListData, charData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerProductionNominalMax(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 8000.0, data) +} diff --git a/uclpp/testhelper_test.go b/uclpp/testhelper_test.go new file mode 100644 index 0000000..dd0a80c --- /dev/null +++ b/uclpp/testhelper_test.go @@ -0,0 +1,182 @@ +package uclpp + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/cemd/api" + eebusapi "github.com/enbility/eebus-go/api" + eebusmocks "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestLPPSuite(t *testing.T) { + suite.Run(t, new(UCLPPSuite)) +} + +type UCLPPSuite struct { + suite.Suite + + sut *UCLPP + + service eebusapi.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *mocks.EntityRemoteInterface + monitoredEntity spineapi.EntityRemoteInterface +} + +func (s *UCLPPSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { +} + +func (s *UCLPPSuite) BeforeTest(suiteName, testName string) { + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := eebusapi.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := eebusmocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := mocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = mocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := mocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + s.sut = NewUCLPP(s.service, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + s.remoteDevice, s.monitoredEntity = setupDevices(s.service, s.T()) +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService eebusapi.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + role model.RoleType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeLoadControl, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeLoadControlLimitDescriptionListData, + model.FunctionTypeLoadControlLimitListData, + }, + }, + {model.FeatureTypeTypeDeviceConfiguration, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, + model.FunctionTypeDeviceConfigurationKeyValueListData, + }, + }, + {model.FeatureTypeTypeDeviceDiagnosis, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeDeviceDiagnosisHeartbeatData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionCharacteristicListData, + }, + }, + } + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: eebusutil.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + Feature: eebusutil.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: eebusutil.Ptr(feature.featureType), + Role: eebusutil.Ptr(feature.role), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeEVSE), + }, + }, + }, + FeatureInformation: featureInformations, + } + + entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + remoteDevice.UpdateDevice(detailedData.DeviceInformation.Description) + + localDevice.AddRemoteDeviceForSki(remoteSki, remoteDevice) + + return remoteDevice, entities[0] +} diff --git a/uclpp/types.go b/uclpp/types.go new file mode 100644 index 0000000..cbac18a --- /dev/null +++ b/uclpp/types.go @@ -0,0 +1,28 @@ +package uclpp + +import "github.com/enbility/cemd/api" + +const ( + // Load control obligation limit data updated + // + // Use `ProductionLimit` to get the current data + // + // Use Case LPC, Scenario 1 + DataUpdateLimit api.EventType = "DataUpdateLimit" + + // Failsafe limit for the produced active (real) power of the + // Controllable System data updated + // + // Use `FailsafeProductionActivePowerLimit` to get the current data + // + // Use Case LPC, Scenario 2 + DataUpdateFailsafeProductionActivePowerLimit api.EventType = "DataUpdateFailsafeProductionActivePowerLimit" + + // Minimum time the Controllable System remains in "failsafe state" unless conditions + // specified in this Use Case permit leaving the "failsafe state" data updated + // + // Use `FailsafeDurationMinimum` to get the current data + // + // Use Case LPC, Scenario 2 + DataUpdateFailsafeDurationMinimum api.EventType = "DataUpdateFailsafeDurationMinimum" +) diff --git a/uclpp/uclpp.go b/uclpp/uclpp.go new file mode 100644 index 0000000..798faf5 --- /dev/null +++ b/uclpp/uclpp.go @@ -0,0 +1,119 @@ +package uclpp + +import ( + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +type UCLPP struct { + service eebusapi.ServiceInterface + + eventCB api.EntityEventCallback + + validEntityTypes []model.EntityTypeType +} + +var _ UCLPPInterface = (*UCLPP)(nil) + +func NewUCLPP(service eebusapi.ServiceInterface, eventCB api.EntityEventCallback) *UCLPP { + uc := &UCLPP{ + service: service, + eventCB: eventCB, + } + + uc.validEntityTypes = []model.EntityTypeType{ + model.EntityTypeTypeEVSE, + model.EntityTypeTypeInverter, + model.EntityTypeTypeSmartEnergyAppliance, + model.EntityTypeTypeSubMeterElectricity, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (c *UCLPP) UseCaseName() model.UseCaseNameType { + return model.UseCaseNameTypeLimitationOfPowerProduction +} + +func (e *UCLPP) AddFeatures() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceDiagnosis, + model.FeatureTypeTypeLoadControl, + model.FeatureTypeTypeDeviceConfiguration, + model.FeatureTypeTypeElectricalConnection, + } + for _, feature := range clientFeatures { + _ = localEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } + + // server features + f := localEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeDeviceDiagnosisHeartbeatData, true, false) +} + +func (e *UCLPP) AddUseCase() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.AddUseCaseSupport( + model.UseCaseActorTypeEnergyGuard, + e.UseCaseName(), + model.SpecificationVersionType("1.0.0"), + "release", + true, + []model.UseCaseScenarioSupportType{1, 2, 3, 4}) +} + +func (e *UCLPP) UpdateUseCaseAvailability(available bool) { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.SetUseCaseAvailability(model.UseCaseActorTypeEnergyGuard, e.UseCaseName(), available) +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *UCLPP) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return false, api.ErrNoCompatibleEntity + } + + // check if the usecase and mandatory scenarios are supported and + // if the required server features are available + if !entity.Device().VerifyUseCaseScenariosAndFeaturesSupport( + model.UseCaseActorTypeEnergyGuard, + e.UseCaseName(), + []model.UseCaseScenarioSupportType{1, 2, 3, 4}, + []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceDiagnosis, + model.FeatureTypeTypeLoadControl, + model.FeatureTypeTypeDeviceConfiguration, + }, + ) { + return false, nil + } + + if _, err := util.DeviceDiagnosis(e.service, entity); err != nil { + return false, eebusapi.ErrFunctionNotSupported + } + + if _, err := util.LoadControl(e.service, entity); err != nil { + return false, eebusapi.ErrFunctionNotSupported + } + + if _, err := util.DeviceConfiguration(e.service, entity); err != nil { + return false, eebusapi.ErrFunctionNotSupported + } + + return true, nil +} diff --git a/uclpp/uclpp_test.go b/uclpp/uclpp_test.go new file mode 100644 index 0000000..0c90cec --- /dev/null +++ b/uclpp/uclpp_test.go @@ -0,0 +1,45 @@ +package uclpp + +import ( + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCLPPSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *UCLPPSuite) Test_IsUseCaseSupported() { + data, err := s.sut.IsUseCaseSupported(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + data, err = s.sut.IsUseCaseSupported(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: eebusutil.Ptr(model.UseCaseActorTypeEnergyGuard), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: eebusutil.Ptr(model.UseCaseNameTypeLimitationOfPowerProduction), + UseCaseAvailable: eebusutil.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1, 2, 3, 4}, + }, + }, + }, + }, + } + + nodemgmtEntity := s.remoteDevice.Entity([]model.AddressEntityType{0}) + nodeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(nodemgmtEntity, model.FeatureTypeTypeNodeManagement, model.RoleTypeSpecial) + fErr := nodeFeature.UpdateData(model.FunctionTypeNodeManagementUseCaseData, ucData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/uclppserver/api.go b/uclppserver/api.go new file mode 100644 index 0000000..c710a89 --- /dev/null +++ b/uclppserver/api.go @@ -0,0 +1,92 @@ +package uclppserver + +import ( + "time" + + "github.com/enbility/cemd/api" + "github.com/enbility/spine-go/model" +) + +//go:generate mockery + +// interface for the Limitation of Power Production UseCase +type UCLPPServerInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // return the current loadcontrol limit data + // + // return values: + // - limit: load limit data + // + // possible errors: + // - ErrDataNotAvailable if no such limit is (yet) available + // - and others + ProductionLimit() (api.LoadLimit, error) + + // set the current loadcontrol limit data + SetProductionLimit(limit api.LoadLimit) (resultErr error) + + // return the currently pending incoming consumption write limits + PendingProductionLimits() map[model.MsgCounterType]api.LoadLimit + + // accept or deny an incoming consumption write limit + // + // parameters: + // - msg: the incoming write message + // - approve: if the write limit for msg should be approved or not + // - reason: the reason why the approval is denied, otherwise an empty string + ApproveOrDenyProductionLimit(msgCounter model.MsgCounterType, approve bool, reason string) + + // Scenario 2 + + // return Failsafe limit for the produced active (real) power of the + // Controllable System. This limit becomes activated in "init" state or "failsafe state". + // + // return values: + // - value: the power limit in W + // - changeable: boolean if the client service can change the limit + FailsafeProductionActivePowerLimit() (value float64, isChangeable bool, resultErr error) + + // set Failsafe limit for the produced active (real) power of the + // Controllable System. This limit becomes activated in "init" state or "failsafe state". + // + // parameters: + // - value: the power limit in W + // - changeable: boolean if the client service can change the limit + SetFailsafeProductionActivePowerLimit(value float64, changeable bool) (resultErr error) + + // return minimum time the Controllable System remains in "failsafe state" unless conditions + // specified in this Use Case permit leaving the "failsafe state" + // + // return values: + // - value: the power limit in W + // - changeable: boolean if the client service can change the limit + FailsafeDurationMinimum() (duration time.Duration, isChangeable bool, resultErr error) + + // set minimum time the Controllable System remains in "failsafe state" unless conditions + // specified in this Use Case permit leaving the "failsafe state" + // + // parameters: + // - duration: has to be >= 2h and <= 24h + // - changeable: boolean if the client service can change this value + SetFailsafeDurationMinimum(duration time.Duration, changeable bool) (resultErr error) + + // Scenario 3 + + // this is automatically covered by the SPINE implementation + + // Scenario 4 + + // return nominal maximum active (real) power the Controllable System is + // allowed to produce due to the customer's contract. + ContractualProductionNominalMax() (float64, error) + + // set nominal maximum active (real) power the Controllable System is + // allowed to produce due to the customer's contract. + // + // parameters: + // - value: contractual nominal max power production in W + SetContractualProductionNominalMax(value float64) (resultErr error) +} diff --git a/uclppserver/events.go b/uclppserver/events.go new file mode 100644 index 0000000..409fbb3 --- /dev/null +++ b/uclppserver/events.go @@ -0,0 +1,157 @@ +package uclppserver + +import ( + "slices" + + "github.com/enbility/cemd/util" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// handle SPINE events +func (e *UCLPPServer) HandleEvent(payload spineapi.EventPayload) { + if util.IsDeviceConnected(payload) { + e.deviceConnected(payload) + return + } + + if !util.IsCompatibleEntity(payload.Entity, e.validEntityTypes) { + return + } + + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + // did we receive a binding to the loadControl server and the + // heartbeatWorkaround is required? + if payload.EventType == spineapi.EventTypeBindingChange && + payload.ChangeType == spineapi.ElementChangeAdd && + payload.LocalFeature != nil && + payload.LocalFeature.Type() == model.FeatureTypeTypeLoadControl && + payload.LocalFeature.Role() == model.RoleTypeServer { + e.subscribeHeartbeatWorkaround(payload) + return + } + + if localEntity == nil || + payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate || + payload.CmdClassifier == nil || + *payload.CmdClassifier != model.CmdClassifierTypeWrite { + return + } + + // the codefactor warning is invalid, as .(type) check can not be replaced with if then + //revive:disable-next-line + switch payload.Data.(type) { + case *model.LoadControlLimitListDataType: + serverF := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + + if payload.Function != model.FunctionTypeLoadControlLimitListData || + payload.LocalFeature != serverF { + return + } + + e.loadControlLimitDataUpdate(payload) + case *model.DeviceConfigurationKeyValueListDataType: + serverF := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + + if payload.Function != model.FunctionTypeDeviceConfigurationKeyValueListData || + payload.LocalFeature != serverF { + return + } + + e.configurationDataUpdate(payload) + } +} + +// a remote device was connected and we know its entities +func (e *UCLPPServer) deviceConnected(payload spineapi.EventPayload) { + if payload.Device == nil { + return + } + + // check if there is a DeviceDiagnosis server on one or more entities + remoteDevice := payload.Device + + var deviceDiagEntites []spineapi.EntityRemoteInterface + + entites := remoteDevice.Entities() + for _, entity := range entites { + if !slices.Contains(e.validEntityTypes, entity.EntityType()) { + continue + } + + deviceDiagF := entity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + if deviceDiagF == nil { + continue + } + + deviceDiagEntites = append(deviceDiagEntites, entity) + } + + // the remote device does not have a DeviceDiagnosis Server, which it should + if len(deviceDiagEntites) == 0 { + return + } + + // we only found one matching entity, as it should be, subscribe + if len(deviceDiagEntites) == 1 { + if localDeviceDiag, err := util.DeviceDiagnosis(e.service, deviceDiagEntites[0]); err == nil { + if _, err := localDeviceDiag.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + if _, err := localDeviceDiag.RequestHeartbeat(); err != nil { + logging.Log().Debug(err) + } + } + + return + } + + // we found more than one matching entity, this is not good + // according to KEO the subscription should be done on the entity that requests a binding to + // the local loadControlLimit server feature + e.heartbeatKeoWorkaround = true +} + +// subscribe to the DeviceDiagnosis Server of the entity that created a binding +func (e *UCLPPServer) subscribeHeartbeatWorkaround(payload spineapi.EventPayload) { + // the workaround is not needed, exit + if !e.heartbeatKeoWorkaround { + return + } + + if localDeviceDiag, err := util.DeviceDiagnosis(e.service, payload.Entity); err == nil { + if _, err := localDeviceDiag.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + if _, err := localDeviceDiag.RequestHeartbeat(); err != nil { + logging.Log().Debug(err) + } + } +} + +// the load control limit data was updated +func (e *UCLPPServer) loadControlLimitDataUpdate(payload spineapi.EventPayload) { + if util.LoadControlLimitsCheckPayloadDataForTypeCategoryDirectionScope( + true, e.service, payload, + model.LoadControlLimitTypeTypeSignDependentAbsValueLimit, + model.LoadControlCategoryTypeObligation, + model.EnergyDirectionTypeProduce, + model.ScopeTypeTypeActivePowerLimit) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateLimit) + } +} + +// the configuration key data of an SMGW was updated +func (e *UCLPPServer) configurationDataUpdate(payload spineapi.EventPayload) { + if util.DeviceConfigurationCheckDataPayloadForKeyName(true, e.service, payload, model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateFailsafeProductionActivePowerLimit) + } + if util.DeviceConfigurationCheckDataPayloadForKeyName(true, e.service, payload, model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateFailsafeDurationMinimum) + } +} diff --git a/uclppserver/events_test.go b/uclppserver/events_test.go new file mode 100644 index 0000000..5146c2f --- /dev/null +++ b/uclppserver/events_test.go @@ -0,0 +1,309 @@ +package uclppserver + +import ( + "fmt" + + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" +) + +func (s *UCLPPServerSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Device = s.monitoredEntity.Device() + payload.Entity = s.monitoredEntity + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDeviceChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeEntityChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.CmdClassifier = eebusutil.Ptr(model.CmdClassifierTypeWrite) + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.Function = model.FunctionTypeLoadControlLimitListData + payload.Data = eebusutil.Ptr(model.LoadControlLimitListDataType{}) + s.sut.HandleEvent(payload) + + payload.LocalFeature = s.loadControlFeature + s.sut.HandleEvent(payload) + + payload.Function = model.FunctionTypeDeviceConfigurationKeyValueListData + payload.Data = eebusutil.Ptr(model.DeviceConfigurationKeyValueListDataType{}) + s.sut.HandleEvent(payload) + + payload.LocalFeature = s.deviceConfigurationFeature + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeBindingChange + payload.ChangeType = spineapi.ElementChangeAdd + payload.LocalFeature = s.loadControlFeature + s.sut.HandleEvent(payload) +} + +func (s *UCLPPServerSuite) Test_deviceConnected() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + + s.sut.deviceConnected(payload) + + // no entities + mockRemoteDevice := mocks.NewDeviceRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().Entities().Return(nil) + payload.Device = mockRemoteDevice + s.sut.deviceConnected(payload) + + // one entity with one DeviceDiagnosis server + payload.Device = s.remoteDevice + s.sut.deviceConnected(payload) + + s.sut.subscribeHeartbeatWorkaround(payload) +} + +func (s *UCLPPServerSuite) Test_multipleDeviceDiagServer() { + // multiple entities each with DeviceDiagnosis server + + payload := spineapi.EventPayload{ + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + role model.RoleType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeLoadControl, + model.RoleTypeClient, + []model.FunctionType{}, + }, + {model.FeatureTypeTypeDeviceConfiguration, + model.RoleTypeClient, + []model.FunctionType{}, + }, + {model.FeatureTypeTypeDeviceDiagnosis, + model.RoleTypeClient, + []model.FunctionType{}, + }, + {model.FeatureTypeTypeDeviceDiagnosis, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeDeviceDiagnosisHeartbeatData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + model.RoleTypeClient, + []model.FunctionType{}, + }, + } + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + // 4 entites + for i := 1; i < 5; i++ { + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: eebusutil.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{model.AddressEntityType(i)}, + Feature: eebusutil.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: eebusutil.Ptr(feature.featureType), + Role: eebusutil.Ptr(feature.role), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeCEM), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{2}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeCEM), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{3}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeCEM), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{4}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeCEM), + }, + }, + }, + FeatureInformation: featureInformations, + } + + _, err := s.remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + s.remoteDevice.UpdateDevice(detailedData.DeviceInformation.Description) + + s.sut.deviceConnected(payload) + + s.sut.subscribeHeartbeatWorkaround(payload) +} + +func (s *UCLPPServerSuite) Test_loadControlLimitDataUpdate() { + localDevice := s.service.LocalDevice() + localEntity := localDevice.EntityForType(model.EntityTypeTypeCEM) + + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.monitoredEntity, + } + s.sut.loadControlLimitDataUpdate(payload) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + LimitType: eebusutil.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitCategory: eebusutil.Ptr(model.LoadControlCategoryTypeObligation), + LimitDirection: eebusutil.Ptr(model.EnergyDirectionTypeProduce), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeActivePowerLimit), + }, + }, + } + + lFeature := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + lFeature.SetData(model.FunctionTypeLoadControlLimitDescriptionListData, descData) + + s.sut.loadControlLimitDataUpdate(payload) + + data := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{}, + } + + payload.Data = data + + s.sut.loadControlLimitDataUpdate(payload) + + data = &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + Value: model.NewScaledNumberType(16), + }, + }, + } + + payload.Data = data + + s.sut.loadControlLimitDataUpdate(payload) +} + +func (s *UCLPPServerSuite) Test_configurationDataUpdate() { + localDevice := s.service.LocalDevice() + localEntity := localDevice.EntityForType(model.EntityTypeTypeCEM) + + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.monitoredEntity, + } + s.sut.configurationDataUpdate(payload) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(1)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit), + }, + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(2)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + }, + }, + } + + lFeature := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + lFeature.SetData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData) + + s.sut.configurationDataUpdate(payload) + + data := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{}, + } + + payload.Data = data + + s.sut.configurationDataUpdate(payload) + + data = &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(1)), + Value: eebusutil.Ptr(model.DeviceConfigurationKeyValueValueType{}), + }, + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(2)), + Value: eebusutil.Ptr(model.DeviceConfigurationKeyValueValueType{}), + }, + }, + } + + payload.Data = data + + s.sut.configurationDataUpdate(payload) +} diff --git a/uclppserver/public.go b/uclppserver/public.go new file mode 100644 index 0000000..d11477d --- /dev/null +++ b/uclppserver/public.go @@ -0,0 +1,269 @@ +package uclppserver + +import ( + "errors" + "time" + + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" +) + +// Scenario 1 + +// return the current production limit data +// +// return values: +// - limit: load limit data +// +// possible errors: +// - ErrDataNotAvailable if no such limit is (yet) available +// - and others +func (e *UCLPPServer) ProductionLimit() (limit api.LoadLimit, resultErr error) { + limit = api.LoadLimit{ + Value: 0.0, + IsChangeable: false, + IsActive: false, + Duration: 0, + } + resultErr = eebusapi.ErrDataNotAvailable + + limidId, err := e.loadControlLimitId() + if err != nil { + return + } + + value := util.GetLocalLimitValueForLimitId(e.service, limidId) + if value.LimitId == nil || value.Value == nil { + return + } + + limit.Value = value.Value.GetValue() + limit.IsChangeable = (value.IsLimitChangeable != nil && *value.IsLimitChangeable) + limit.IsActive = (value.IsLimitActive != nil && *value.IsLimitActive) + if value.TimePeriod != nil && value.TimePeriod.EndTime != nil { + if duration, err := value.TimePeriod.EndTime.GetTimeDuration(); err == nil { + limit.Duration = duration + } + } + + return limit, nil +} + +// set the current production limit data +func (e *UCLPPServer) SetProductionLimit(limit api.LoadLimit) (resultErr error) { + resultErr = eebusapi.ErrDataNotAvailable + + limidId, err := e.loadControlLimitId() + if err != nil { + return + } + + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + loadControl := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + if loadControl == nil { + return + } + + limitData := model.LoadControlLimitDataType{ + LimitId: eebusutil.Ptr(limidId), + IsLimitChangeable: eebusutil.Ptr(limit.IsChangeable), + IsLimitActive: eebusutil.Ptr(limit.IsActive), + Value: model.NewScaledNumberType(limit.Value), + } + if limit.Duration > 0 { + limitData.TimePeriod = &model.TimePeriodType{ + EndTime: model.NewAbsoluteOrRelativeTimeTypeFromDuration(limit.Duration), + } + } + // TODO: this overwrites LPC data as well + limits := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{limitData}, + } + + loadControl.SetData(model.FunctionTypeLoadControlLimitListData, limits) + + return nil +} + +// return the currently pending incoming consumption write limits +func (e *UCLPPServer) PendingProductionLimits() map[model.MsgCounterType]api.LoadLimit { + result := make(map[model.MsgCounterType]api.LoadLimit) + + limitId, err := e.loadControlLimitId() + if err != nil { + return result + } + + e.pendingMux.Lock() + defer e.pendingMux.Unlock() + + for key, msg := range e.pendingLimits { + data := msg.Cmd.LoadControlLimitListData + + // elements are only added to the map if all required fields exist + // therefor not check for these are needed here + + // find the item which contains the limit for this usecase + for _, item := range data.LoadControlLimitData { + if item.LimitId == nil || + limitId != *item.LimitId { + continue + } + + limit := api.LoadLimit{} + + if item.TimePeriod != nil { + if duration, err := item.TimePeriod.GetDuration(); err == nil { + limit.Duration = duration + } + } + + if item.IsLimitActive != nil { + limit.IsActive = *item.IsLimitActive + } + + if item.Value != nil { + limit.Value = item.Value.GetValue() + } + + result[key] = limit + } + } + + return result +} + +// accept or deny an incoming consumption write limit +// +// use PendingProductionLimits to get the list of currently pending requests +func (e *UCLPPServer) ApproveOrDenyProductionLimit(msgCounter model.MsgCounterType, approve bool, reason string) { + e.pendingMux.Lock() + defer e.pendingMux.Unlock() + + msg, ok := e.pendingLimits[msgCounter] + if !ok { + return + } + + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + f := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + + result := model.ErrorType{ + ErrorNumber: model.ErrorNumberType(0), + } + if !approve { + result.ErrorNumber = model.ErrorNumberType(7) + result.Description = eebusutil.Ptr(model.DescriptionType(reason)) + } + f.ApproveOrDenyWrite(msg, result) +} + +// Scenario 2 + +// return Failsafe limit for the produced active (real) power of the +// Controllable System. This limit becomes activated in "init" state or "failsafe state". +func (e *UCLPPServer) FailsafeProductionActivePowerLimit() (limit float64, isChangeable bool, resultErr error) { + limit = 0 + isChangeable = false + resultErr = eebusapi.ErrDataNotAvailable + + keyName := model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit + keyData := util.GetLocalDeviceConfigurationKeyValueForKeyName(e.service, keyName) + if keyData.KeyId == nil || keyData.Value == nil || keyData.Value.ScaledNumber == nil { + return + } + + limit = keyData.Value.ScaledNumber.GetValue() + isChangeable = (keyData.IsValueChangeable != nil && *keyData.IsValueChangeable) + resultErr = nil + return +} + +// set Failsafe limit for the produced active (real) power of the +// Controllable System. This limit becomes activated in "init" state or "failsafe state". +func (e *UCLPPServer) SetFailsafeProductionActivePowerLimit(value float64, changeable bool) error { + keyName := model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit + keyValue := model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(value), + } + + return util.SetLocalDeviceConfigurationKeyValue(e.service, keyName, changeable, keyValue) +} + +// return minimum time the Controllable System remains in "failsafe state" unless conditions +// specified in this Use Case permit leaving the "failsafe state" +func (e *UCLPPServer) FailsafeDurationMinimum() (duration time.Duration, isChangeable bool, resultErr error) { + duration = 0 + isChangeable = false + resultErr = eebusapi.ErrDataNotAvailable + + keyName := model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum + keyData := util.GetLocalDeviceConfigurationKeyValueForKeyName(e.service, keyName) + if keyData.KeyId == nil || keyData.Value == nil || keyData.Value.Duration == nil { + return + } + + durationValue, err := keyData.Value.Duration.GetTimeDuration() + if err != nil { + return + } + + duration = durationValue + isChangeable = (keyData.IsValueChangeable != nil && *keyData.IsValueChangeable) + resultErr = nil + return +} + +// set minimum time the Controllable System remains in "failsafe state" unless conditions +// specified in this Use Case permit leaving the "failsafe state" +// +// parameters: +// - duration: has to be >= 2h and <= 24h +// - changeable: boolean if the client service can change this value +func (e *UCLPPServer) SetFailsafeDurationMinimum(duration time.Duration, changeable bool) error { + if duration < time.Duration(time.Hour*2) || duration > time.Duration(time.Hour*24) { + return errors.New("duration outside of allowed range") + } + keyName := model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum + keyValue := model.DeviceConfigurationKeyValueValueType{ + Duration: model.NewDurationType(duration), + } + + return util.SetLocalDeviceConfigurationKeyValue(e.service, keyName, changeable, keyValue) +} + +// Scenario 4 + +// return nominal maximum active (real) power the Controllable System is +// allowed to produce due to the customer's contract. +func (e *UCLPPServer) ContractualProductionNominalMax() (value float64, resultErr error) { + value = 0 + resultErr = eebusapi.ErrDataNotAvailable + + charData := util.GetLocalElectricalConnectionCharacteristicForContextType( + e.service, + model.ElectricalConnectionCharacteristicContextTypeEntity, + model.ElectricalConnectionCharacteristicTypeTypeContractualProductionNominalMax, + ) + if charData.CharacteristicId == nil || charData.Value == nil { + return + } + + return charData.Value.GetValue(), nil +} + +// set nominal maximum active (real) power the Controllable System is +// allowed to produce due to the customer's contract. +func (e *UCLPPServer) SetContractualProductionNominalMax(value float64) error { + return util.SetLocalElectricalConnectionCharacteristicForContextType( + e.service, + model.ElectricalConnectionCharacteristicContextTypeEntity, + model.ElectricalConnectionCharacteristicTypeTypeContractualProductionNominalMax, + value, + ) +} diff --git a/uclppserver/public_test.go b/uclppserver/public_test.go new file mode 100644 index 0000000..c022d84 --- /dev/null +++ b/uclppserver/public_test.go @@ -0,0 +1,116 @@ +package uclppserver + +import ( + "time" + + "github.com/enbility/cemd/api" + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCLPPServerSuite) Test_LoadControlLimit() { + limit, err := s.sut.ProductionLimit() + assert.Equal(s.T(), 0.0, limit.Value) + assert.NotNil(s.T(), err) + + newLimit := api.LoadLimit{ + Duration: time.Duration(time.Hour * 2), + IsActive: true, + IsChangeable: true, + Value: 16, + } + err = s.sut.SetProductionLimit(newLimit) + assert.Nil(s.T(), err) + + limit, err = s.sut.ProductionLimit() + assert.Equal(s.T(), 16.0, limit.Value) + assert.Nil(s.T(), err) +} + +func (s *UCLPPServerSuite) Test_PendingProductionLimits() { + data := s.sut.PendingProductionLimits() + assert.Equal(s.T(), 0, len(data)) + + msgCounter := model.MsgCounterType(500) + + msg := &spineapi.Message{ + RequestHeader: &model.HeaderType{ + MsgCounter: eebusutil.Ptr(msgCounter), + }, + Cmd: model.CmdType{ + LoadControlLimitListData: &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + IsLimitActive: eebusutil.Ptr(true), + Value: model.NewScaledNumberType(1000), + TimePeriod: model.NewTimePeriodTypeWithRelativeEndTime(time.Minute * 2), + }, + }, + }, + }, + DeviceRemote: s.remoteDevice, + EntityRemote: s.monitoredEntity, + } + + s.sut.loadControlWriteCB(msg) + + data = s.sut.PendingProductionLimits() + assert.Equal(s.T(), 1, len(data)) + + s.sut.ApproveOrDenyProductionLimit(model.MsgCounterType(499), true, "") + + s.sut.ApproveOrDenyProductionLimit(msgCounter, false, "leave me alone") +} + +func (s *UCLPPServerSuite) Test_Failsafe() { + limit, changeable, err := s.sut.FailsafeProductionActivePowerLimit() + assert.Equal(s.T(), 0.0, limit) + assert.Equal(s.T(), false, changeable) + assert.NotNil(s.T(), err) + + err = s.sut.SetFailsafeProductionActivePowerLimit(10, true) + assert.Nil(s.T(), err) + + limit, changeable, err = s.sut.FailsafeProductionActivePowerLimit() + assert.Equal(s.T(), 10.0, limit) + assert.Equal(s.T(), true, changeable) + assert.Nil(s.T(), err) + + // The actual tests of the functionality is located in the util package + duration, changeable, err := s.sut.FailsafeDurationMinimum() + assert.Equal(s.T(), time.Duration(0), duration) + assert.Equal(s.T(), false, changeable) + assert.NotNil(s.T(), err) + + err = s.sut.SetFailsafeDurationMinimum(time.Duration(time.Hour*1), true) + assert.NotNil(s.T(), err) + + err = s.sut.SetFailsafeDurationMinimum(time.Duration(time.Hour*2), true) + assert.Nil(s.T(), err) + + limit, changeable, err = s.sut.FailsafeProductionActivePowerLimit() + assert.Equal(s.T(), 10.0, limit) + assert.Equal(s.T(), true, changeable) + assert.Nil(s.T(), err) + + duration, changeable, err = s.sut.FailsafeDurationMinimum() + assert.Equal(s.T(), time.Duration(time.Hour*2), duration) + assert.Equal(s.T(), true, changeable) + assert.Nil(s.T(), err) +} + +func (s *UCLPPServerSuite) Test_ContractualProductionNominalMax() { + value, err := s.sut.ContractualProductionNominalMax() + assert.Equal(s.T(), 0.0, value) + assert.NotNil(s.T(), err) + + err = s.sut.SetContractualProductionNominalMax(10) + assert.Nil(s.T(), err) + + value, err = s.sut.ContractualProductionNominalMax() + assert.Equal(s.T(), 10.0, value) + assert.Nil(s.T(), err) +} diff --git a/uclppserver/testhelper_test.go b/uclppserver/testhelper_test.go new file mode 100644 index 0000000..fd9e33d --- /dev/null +++ b/uclppserver/testhelper_test.go @@ -0,0 +1,197 @@ +package uclppserver + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/cemd/api" + eebusapi "github.com/enbility/eebus-go/api" + eebusmocks "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestLPPServerSuite(t *testing.T) { + suite.Run(t, new(UCLPPServerSuite)) +} + +type UCLPPServerSuite struct { + suite.Suite + + sut *UCLPPServer + + service eebusapi.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *mocks.EntityRemoteInterface + monitoredEntity spineapi.EntityRemoteInterface + loadControlFeature, + deviceDiagnosisFeature, + deviceConfigurationFeature spineapi.FeatureLocalInterface +} + +func (s *UCLPPServerSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { +} + +func (s *UCLPPServerSuite) BeforeTest(suiteName, testName string) { + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := eebusapi.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := eebusmocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := mocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = mocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := mocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + s.sut = NewUCLPP(s.service, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + localEntity := s.sut.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + s.loadControlFeature = localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + s.deviceDiagnosisFeature = localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + s.deviceConfigurationFeature = localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + + s.remoteDevice, s.monitoredEntity = setupDevices(s.service, s.T()) +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService eebusapi.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + localEntity := localDevice.EntityForType(model.EntityTypeTypeCEM) + + f := spine.NewFeatureLocal(1, localEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeLoadControlLimitDescriptionListData, true, false) + f.AddFunctionType(model.FunctionTypeLoadControlLimitListData, true, true) + localEntity.AddFeature(f) + f = spine.NewFeatureLocal(2, localEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeElectricalConnectionParameterDescriptionListData, true, false) + f.AddFunctionType(model.FunctionTypeElectricalConnectionPermittedValueSetListData, true, false) + f.AddFunctionType(model.FunctionTypeElectricalConnectionCharacteristicListData, true, true) + localEntity.AddFeature(f) + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + role model.RoleType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeLoadControl, + model.RoleTypeClient, + []model.FunctionType{}, + }, + {model.FeatureTypeTypeDeviceConfiguration, + model.RoleTypeClient, + []model.FunctionType{}, + }, + {model.FeatureTypeTypeDeviceDiagnosis, + model.RoleTypeClient, + []model.FunctionType{}, + }, + {model.FeatureTypeTypeDeviceDiagnosis, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeDeviceDiagnosisHeartbeatData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + model.RoleTypeClient, + []model.FunctionType{}, + }, + } + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: eebusutil.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + Feature: eebusutil.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: eebusutil.Ptr(feature.featureType), + Role: eebusutil.Ptr(feature.role), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeGridGuard), + }, + }, + }, + FeatureInformation: featureInformations, + } + + entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + remoteDevice.UpdateDevice(detailedData.DeviceInformation.Description) + + localDevice.AddRemoteDeviceForSki(remoteSki, remoteDevice) + + return remoteDevice, entities[0] +} diff --git a/uclppserver/types.go b/uclppserver/types.go new file mode 100644 index 0000000..e5df6d8 --- /dev/null +++ b/uclppserver/types.go @@ -0,0 +1,36 @@ +package uclppserver + +import "github.com/enbility/cemd/api" + +const ( + // Load control obligation limit data update received + // + // Use `ProductionLimit` to get the current data + // + // Use Case LPC, Scenario 1 + DataUpdateLimit api.EventType = "DataUpdateLimit" + + // An incoming load control obligation limit needs to be approved or denied + // + // Use `PendingProductionLimits` to get the currently pending write approval requests + // and invoke `ApproveOrDenyProductionLimit` for each + // + // Use Case LPC, Scenario 1 + WriteApprovalRequired api.EventType = "WriteApprovalRequired" + + // Failsafe limit for the produced active (real) power of the + // Controllable System data update received + // + // Use `FailsafeProductionActivePowerLimit` to get the current data + // + // Use Case LPC, Scenario 2 + DataUpdateFailsafeProductionActivePowerLimit api.EventType = "DataUpdateFailsafeProductionActivePowerLimit" + + // Minimum time the Controllable System remains in "failsafe state" unless conditions + // specified in this Use Case permit leaving the "failsafe state" data update received + // + // Use `FailsafeDurationMinimum` to get the current data + // + // Use Case LPC, Scenario 2 + DataUpdateFailsafeDurationMinimum api.EventType = "DataUpdateFailsafeDurationMinimum" +) diff --git a/uclppserver/uclpp.go b/uclppserver/uclpp.go new file mode 100644 index 0000000..9543d94 --- /dev/null +++ b/uclppserver/uclpp.go @@ -0,0 +1,304 @@ +package uclppserver + +import ( + "errors" + "sync" + + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +type UCLPPServer struct { + service eebusapi.ServiceInterface + + eventCB api.EntityEventCallback + + validEntityTypes []model.EntityTypeType + + pendingMux sync.Mutex + pendingLimits map[model.MsgCounterType]*spineapi.Message + + heartbeatKeoWorkaround bool // required because KEO Stack uses multiple identical entities for the same functionality, and it is not clear which to use +} + +var _ UCLPPServerInterface = (*UCLPPServer)(nil) + +func NewUCLPP(service eebusapi.ServiceInterface, eventCB api.EntityEventCallback) *UCLPPServer { + uc := &UCLPPServer{ + service: service, + eventCB: eventCB, + pendingLimits: make(map[model.MsgCounterType]*spineapi.Message), + } + + uc.validEntityTypes = []model.EntityTypeType{ + model.EntityTypeTypeGridGuard, + model.EntityTypeTypeCEM, // KEO uses this entity type for an SMGW whysoever + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (c *UCLPPServer) UseCaseName() model.UseCaseNameType { + return model.UseCaseNameTypeLimitationOfPowerProduction +} + +func (e *UCLPPServer) loadControlLimitId() (limitid model.LoadControlLimitIdType, err error) { + limitid = model.LoadControlLimitIdType(0) + err = errors.New("not found") + + descriptions := util.GetLocalLimitDescriptionsForTypeCategoryDirectionScope( + e.service, + model.LoadControlLimitTypeTypeSignDependentAbsValueLimit, + model.LoadControlCategoryTypeObligation, + model.EnergyDirectionTypeProduce, + model.ScopeTypeTypeActivePowerLimit, + ) + if len(descriptions) != 1 || descriptions[0].LimitId == nil { + return + } + description := descriptions[0] + + if description.LimitId == nil { + return + } + + return *description.LimitId, nil +} + +// callback invoked on incoming write messages to this +// loadcontrol server feature. +// the implementation only considers write messages for this use case and +// approves all others +func (e *UCLPPServer) loadControlWriteCB(msg *spineapi.Message) { + e.pendingMux.Lock() + defer e.pendingMux.Unlock() + + if msg.RequestHeader == nil || msg.RequestHeader.MsgCounter == nil || + msg.Cmd.LoadControlLimitListData == nil { + return + } + + limitId, err := e.loadControlLimitId() + if err != nil { + return + } + + data := msg.Cmd.LoadControlLimitListData + + // we assume there is always only one limit + if data == nil || data.LoadControlLimitData == nil || + len(data.LoadControlLimitData) == 0 { + return + } + + // check if there is a matching limitId in the data + for _, item := range data.LoadControlLimitData { + if item.LimitId == nil || + limitId != *item.LimitId { + continue + } + + if _, ok := e.pendingLimits[*msg.RequestHeader.MsgCounter]; !ok { + e.pendingLimits[*msg.RequestHeader.MsgCounter] = msg + e.eventCB(msg.DeviceRemote.Ski(), msg.DeviceRemote, msg.EntityRemote, WriteApprovalRequired) + return + } + } + + // approve, because this is no request for this usecase + go e.ApproveOrDenyProductionLimit(*msg.RequestHeader.MsgCounter, true, "") +} + +func (e *UCLPPServer) AddFeatures() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + // client features + _ = localEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeClient) + + // server features + f := localEntity.GetOrAddFeature(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeLoadControlLimitDescriptionListData, true, false) + f.AddFunctionType(model.FunctionTypeLoadControlLimitListData, true, true) + _ = f.AddWriteApprovalCallback(e.loadControlWriteCB) + + var limitId model.LoadControlLimitIdType = 0 + // get the highest limitId + loadControlDesc, err := spine.LocalFeatureDataCopyOfType[*model.LoadControlLimitDescriptionListDataType]( + f, model.FunctionTypeLoadControlLimitDescriptionListData) + if err == nil && loadControlDesc.LoadControlLimitDescriptionData != nil { + for _, desc := range loadControlDesc.LoadControlLimitDescriptionData { + if desc.LimitId != nil && *desc.LimitId >= limitId { + limitId++ + } + } + } + + if loadControlDesc == nil || len(loadControlDesc.LoadControlLimitDescriptionData) == 0 { + loadControlDesc = &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{}, + } + } + + newLimitDesc := model.LoadControlLimitDescriptionDataType{ + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(limitId)), + LimitType: eebusutil.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitCategory: eebusutil.Ptr(model.LoadControlCategoryTypeObligation), + LimitDirection: eebusutil.Ptr(model.EnergyDirectionTypeProduce), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), // This is a fake Measurement ID, as there is no Electrical Connection server defined, it can't provide any meaningful. But KEO requires this to be set :( + Unit: eebusutil.Ptr(model.UnitOfMeasurementTypeW), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeActivePowerLimit), + } + loadControlDesc.LoadControlLimitDescriptionData = append(loadControlDesc.LoadControlLimitDescriptionData, newLimitDesc) + f.SetData(model.FunctionTypeLoadControlLimitDescriptionListData, loadControlDesc) + + f = localEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, true, false) + f.AddFunctionType(model.FunctionTypeDeviceConfigurationKeyValueListData, true, true) + + var configId model.DeviceConfigurationKeyIdType = 0 + // get the highest keyId + deviceConfigDesc, err := spine.LocalFeatureDataCopyOfType[*model.DeviceConfigurationKeyValueDescriptionListDataType]( + f, model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData) + if err == nil && deviceConfigDesc.DeviceConfigurationKeyValueDescriptionData != nil { + for _, desc := range deviceConfigDesc.DeviceConfigurationKeyValueDescriptionData { + if desc.KeyId != nil && *desc.KeyId >= configId { + configId++ + } + } + } + + if deviceConfigDesc == nil || len(deviceConfigDesc.DeviceConfigurationKeyValueDescriptionData) == 0 { + deviceConfigDesc = &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{}, + } + } + + newConfigs := []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit), + ValueType: eebusutil.Ptr(model.DeviceConfigurationKeyValueTypeTypeScaledNumber), + Unit: eebusutil.Ptr(model.UnitOfMeasurementTypeW), + }, + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId + 1)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + ValueType: eebusutil.Ptr(model.DeviceConfigurationKeyValueTypeTypeDuration), + }, + } + deviceConfigDesc.DeviceConfigurationKeyValueDescriptionData = append(deviceConfigDesc.DeviceConfigurationKeyValueDescriptionData, newConfigs...) + f.SetData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, deviceConfigDesc) + + configData, err := spine.LocalFeatureDataCopyOfType[*model.DeviceConfigurationKeyValueListDataType](f, model.FunctionTypeDeviceConfigurationKeyValueListData) + if err != nil || configData == nil || len(configData.DeviceConfigurationKeyValueData) == 0 { + configData = &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{}, + } + } + + newConfigData := []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId)), + IsValueChangeable: eebusutil.Ptr(true), + }, + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId + 1)), + IsValueChangeable: eebusutil.Ptr(true), + }, + } + + configData.DeviceConfigurationKeyValueData = append(configData.DeviceConfigurationKeyValueData, newConfigData...) + f.SetData(model.FunctionTypeDeviceConfigurationKeyValueListData, configData) + + f = localEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeDeviceDiagnosisHeartbeatData, true, false) + + f = localEntity.GetOrAddFeature(model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeElectricalConnectionCharacteristicListData, true, false) + + var elCharId model.ElectricalConnectionCharacteristicIdType = 0 + // get the highest CharacteristicId + elCharData, err := spine.LocalFeatureDataCopyOfType[*model.ElectricalConnectionCharacteristicListDataType]( + f, model.FunctionTypeElectricalConnectionCharacteristicListData) + if err == nil && elCharData.ElectricalConnectionCharacteristicData != nil { + for _, desc := range elCharData.ElectricalConnectionCharacteristicData { + if desc.CharacteristicId != nil && *desc.CharacteristicId >= elCharId { + elCharId++ + } + } + } + + if err != nil || configData == nil || len(configData.DeviceConfigurationKeyValueData) == 0 { + elCharData = &model.ElectricalConnectionCharacteristicListDataType{ + ElectricalConnectionCharacteristicData: []model.ElectricalConnectionCharacteristicDataType{}, + } + } + + // ElectricalConnectionId and ParameterId should be identical to the ones used + // in a MPC Server role implementation, which is not done here (yet) + newCharData := model.ElectricalConnectionCharacteristicDataType{ + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(0)), + CharacteristicId: eebusutil.Ptr(elCharId), + CharacteristicContext: eebusutil.Ptr(model.ElectricalConnectionCharacteristicContextTypeEntity), + CharacteristicType: eebusutil.Ptr(model.ElectricalConnectionCharacteristicTypeTypeContractualProductionNominalMax), + Unit: eebusutil.Ptr(model.UnitOfMeasurementTypeW), + } + elCharData.ElectricalConnectionCharacteristicData = append(elCharData.ElectricalConnectionCharacteristicData, newCharData) + f.SetData(model.FunctionTypeElectricalConnectionCharacteristicListData, elCharData) +} + +func (e *UCLPPServer) AddUseCase() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.AddUseCaseSupport( + model.UseCaseActorTypeControllableSystem, + e.UseCaseName(), + model.SpecificationVersionType("1.0.0"), + "release", + true, + []model.UseCaseScenarioSupportType{1, 2, 3, 4}) +} + +func (e *UCLPPServer) UpdateUseCaseAvailability(available bool) { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.SetUseCaseAvailability(model.UseCaseActorTypeControllableSystem, e.UseCaseName(), available) +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *UCLPPServer) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return false, api.ErrNoCompatibleEntity + } + + // check if the usecase and mandatory scenarios are supported and + // if the required server features are available + if !entity.Device().VerifyUseCaseScenariosAndFeaturesSupport( + model.UseCaseActorTypeEnergyGuard, + e.UseCaseName(), + []model.UseCaseScenarioSupportType{1, 2, 3, 4}, + []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceDiagnosis, + }, + ) { + return false, nil + } + + if _, err := util.DeviceDiagnosis(e.service, entity); err != nil { + return false, eebusapi.ErrFunctionNotSupported + } + + return true, nil +} diff --git a/uclppserver/uclpp_test.go b/uclppserver/uclpp_test.go new file mode 100644 index 0000000..e5974f4 --- /dev/null +++ b/uclppserver/uclpp_test.go @@ -0,0 +1,100 @@ +package uclppserver + +import ( + "time" + + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCLPPServerSuite) Test_loadControlWriteCB() { + msg := &spineapi.Message{} + + s.sut.loadControlWriteCB(msg) + + msg = &spineapi.Message{ + RequestHeader: &model.HeaderType{ + MsgCounter: eebusutil.Ptr(model.MsgCounterType(500)), + }, + Cmd: model.CmdType{ + LoadControlLimitListData: &model.LoadControlLimitListDataType{}, + }, + DeviceRemote: s.remoteDevice, + EntityRemote: s.monitoredEntity, + } + + s.sut.loadControlWriteCB(msg) + + msg.Cmd = model.CmdType{ + LoadControlLimitListData: &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{}, + }, + } + + s.sut.loadControlWriteCB(msg) + + msg.Cmd = model.CmdType{ + LoadControlLimitListData: &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + {}, + }, + }, + } + + s.sut.loadControlWriteCB(msg) + + msg.Cmd = model.CmdType{ + LoadControlLimitListData: &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + IsLimitActive: eebusutil.Ptr(true), + Value: model.NewScaledNumberType(1000), + TimePeriod: model.NewTimePeriodTypeWithRelativeEndTime(time.Minute * 2), + }, + }, + }, + } + + s.sut.loadControlWriteCB(msg) +} + +func (s *UCLPPServerSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *UCLPPServerSuite) Test_IsUseCaseSupported() { + data, err := s.sut.IsUseCaseSupported(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + data, err = s.sut.IsUseCaseSupported(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: eebusutil.Ptr(model.UseCaseActorTypeEnergyGuard), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: eebusutil.Ptr(model.UseCaseNameTypeLimitationOfPowerProduction), + UseCaseAvailable: eebusutil.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1, 2, 3, 4}, + }, + }, + }, + }, + } + + nodemgmtEntity := s.remoteDevice.Entity([]model.AddressEntityType{0}) + nodeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(nodemgmtEntity, model.FeatureTypeTypeNodeManagement, model.RoleTypeSpecial) + fErr := nodeFeature.UpdateData(model.FunctionTypeNodeManagementUseCaseData, ucData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/ucmgcp/api.go b/ucmgcp/api.go new file mode 100644 index 0000000..9793c8f --- /dev/null +++ b/ucmgcp/api.go @@ -0,0 +1,87 @@ +package ucmgcp + +import ( + "github.com/enbility/cemd/api" + spineapi "github.com/enbility/spine-go/api" +) + +//go:generate mockery + +// interface for the Monitoring of Grid Connection Point UseCase +type UCMGCPInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // return the current power limitation factor + // + // parameters: + // - entity: the entity of the device (e.g. SMGW) + // + // possible errors: + // - ErrDataNotAvailable if no such limit is (yet) available + // - and others + PowerLimitationFactor(entity spineapi.EntityRemoteInterface) (float64, error) + + // Scenario 2 + + // return the momentary power consumption or production at the grid connection point + // + // parameters: + // - entity: the entity of the device (e.g. SMGW) + // + // return values: + // - positive values are used for consumption + // - negative values are used for production + Power(entity spineapi.EntityRemoteInterface) (float64, error) + + // Scenario 3 + + // return the total feed in energy at the grid connection point + // + // parameters: + // - entity: the entity of the device (e.g. SMGW) + // + // return values: + // - negative values are used for production + EnergyFeedIn(entity spineapi.EntityRemoteInterface) (float64, error) + + // Scenario 4 + + // return the total consumption energy at the grid connection point + // + // parameters: + // - entity: the entity of the device (e.g. SMGW) + // + // return values: + // - positive values are used for consumption + EnergyConsumed(entity spineapi.EntityRemoteInterface) (float64, error) + + // Scenario 5 + + // return the momentary current consumption or production at the grid connection point + // + // parameters: + // - entity: the entity of the device (e.g. SMGW) + // + // return values: + // - positive values are used for consumption + // - negative values are used for production + CurrentPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) + + // Scenario 6 + + // return the voltage phase details at the grid connection point + // + // parameters: + // - entity: the entity of the device (e.g. SMGW) + VoltagePerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) + + // Scenario 7 + + // return frequency at the grid connection point + // + // parameters: + // - entity: the entity of the device (e.g. SMGW) + Frequency(entity spineapi.EntityRemoteInterface) (float64, error) +} diff --git a/ucmgcp/events.go b/ucmgcp/events.go new file mode 100644 index 0000000..272b35c --- /dev/null +++ b/ucmgcp/events.go @@ -0,0 +1,142 @@ +package ucmgcp + +import ( + "github.com/enbility/cemd/util" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// handle SPINE events +func (e *UCMGCP) HandleEvent(payload spineapi.EventPayload) { + // only about events from an SGMW entity or device changes for this remote device + + if !util.IsCompatibleEntity(payload.Entity, e.validEntityTypes) { + return + } + + if util.IsEntityConnected(payload) { + e.gridConnected(payload.Entity) + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + + switch payload.Data.(type) { + case *model.DeviceConfigurationKeyValueDescriptionListDataType: + e.gridConfigurationDescriptionDataUpdate(payload.Entity) + case *model.DeviceConfigurationKeyValueListDataType: + e.gridConfigurationDataUpdate(payload) + case *model.MeasurementDescriptionListDataType: + e.gridMeasurementDescriptionDataUpdate(payload.Entity) + case *model.MeasurementListDataType: + e.gridMeasurementDataUpdate(payload) + } +} + +// process required steps when a grid device is connected +func (e *UCMGCP) gridConnected(entity spineapi.EntityRemoteInterface) { + if deviceConfiguration, err := util.DeviceConfiguration(e.service, entity); err == nil { + if _, err := deviceConfiguration.Subscribe(); err != nil { + logging.Log().Error(err) + } + + // get configuration data + if _, err := deviceConfiguration.RequestDescriptions(); err != nil { + logging.Log().Error(err) + } + } + + if electricalConnection, err := util.ElectricalConnection(e.service, entity); err == nil { + if _, err := electricalConnection.Subscribe(); err != nil { + logging.Log().Error(err) + } + + // get electrical connection parameter + if _, err := electricalConnection.RequestDescriptions(); err != nil { + logging.Log().Error(err) + } + + if _, err := electricalConnection.RequestParameterDescriptions(); err != nil { + logging.Log().Error(err) + } + } + + if measurement, err := util.Measurement(e.service, entity); err == nil { + if _, err := measurement.Subscribe(); err != nil { + logging.Log().Error(err) + } + + // get measurement parameters + if _, err := measurement.RequestDescriptions(); err != nil { + logging.Log().Error(err) + } + + if _, err := measurement.RequestConstraints(); err != nil { + logging.Log().Error(err) + } + } +} + +// the configuration key description data of an SMGW was updated +func (e *UCMGCP) gridConfigurationDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if deviceConfiguration, err := util.DeviceConfiguration(e.service, entity); err == nil { + // key value descriptions received, now get the data + if _, err := deviceConfiguration.RequestKeyValues(); err != nil { + logging.Log().Error("Error getting configuration key values:", err) + } + } +} + +// the configuration key data of an SMGW was updated +func (e *UCMGCP) gridConfigurationDataUpdate(payload spineapi.EventPayload) { + if _, err := e.PowerLimitationFactor(payload.Entity); err == nil { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePowerLimitationFactor) + } +} + +// the measurement descriptiondata of an SMGW was updated +func (e *UCMGCP) gridMeasurementDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if measurement, err := util.Measurement(e.service, entity); err == nil { + // measurement descriptions received, now get the data + if _, err := measurement.RequestValues(); err != nil { + logging.Log().Error("Error getting measurement list values:", err) + } + } +} + +// the measurement data of an SMGW was updated +func (e *UCMGCP) gridMeasurementDataUpdate(payload spineapi.EventPayload) { + // Scenario 2 + if util.MeasurementCheckPayloadDataForScope(e.service, payload, model.ScopeTypeTypeACPowerTotal) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePower) + } + + // Scenario 3 + if util.MeasurementCheckPayloadDataForScope(e.service, payload, model.ScopeTypeTypeGridFeedIn) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateEnergyFeedIn) + } + + // Scenario 4 + if util.MeasurementCheckPayloadDataForScope(e.service, payload, model.ScopeTypeTypeGridConsumption) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateEnergyConsumed) + } + + // Scenario 5 + if util.MeasurementCheckPayloadDataForScope(e.service, payload, model.ScopeTypeTypeACCurrent) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateCurrentPerPhase) + } + + // Scenario 6 + if util.MeasurementCheckPayloadDataForScope(e.service, payload, model.ScopeTypeTypeACVoltage) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateVoltagePerPhase) + } + + // Scenario 7 + if util.MeasurementCheckPayloadDataForScope(e.service, payload, model.ScopeTypeTypeACFrequency) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateFrequency) + } +} diff --git a/ucmgcp/events_test.go b/ucmgcp/events_test.go new file mode 100644 index 0000000..3587b77 --- /dev/null +++ b/ucmgcp/events_test.go @@ -0,0 +1,160 @@ +package ucmgcp + +import ( + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCMGCPSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.smgwEntity + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeEntityChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.Data = eebusutil.Ptr(model.DeviceConfigurationKeyValueDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.MeasurementDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.MeasurementListDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *UCMGCPSuite) Test_Failures() { + s.sut.gridConnected(s.mockRemoteEntity) + + s.sut.gridConfigurationDescriptionDataUpdate(s.mockRemoteEntity) + + s.sut.gridMeasurementDescriptionDataUpdate(s.mockRemoteEntity) +} + +func (s *UCMGCPSuite) Test_gridConfigurationDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.smgwEntity, + } + s.sut.gridConfigurationDataUpdate(payload) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypePvCurtailmentLimitFactor), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(10), + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.gridConfigurationDataUpdate(payload) +} + +func (s *UCMGCPSuite) Test_gridMeasurementDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.smgwEntity, + } + s.sut.gridMeasurementDataUpdate(payload) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeGridFeedIn), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeGridConsumption), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(3)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACCurrent), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(4)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACVoltage), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(5)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACFrequency), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.gridMeasurementDataUpdate(payload) + + data := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(3)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(4)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(5)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + payload.Data = data + + s.sut.gridMeasurementDataUpdate(payload) +} diff --git a/ucmgcp/public.go b/ucmgcp/public.go new file mode 100644 index 0000000..de6de52 --- /dev/null +++ b/ucmgcp/public.go @@ -0,0 +1,215 @@ +package ucmgcp + +import ( + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// Scenario 1 + +// return the current power limitation factor +// +// possible errors: +// - ErrDataNotAvailable if no such limit is (yet) available +// - and others +func (e *UCMGCP) PowerLimitationFactor(entity spineapi.EntityRemoteInterface) (float64, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + + measurement, err := util.Measurement(e.service, entity) + if err != nil || measurement == nil { + return 0, err + } + + keyname := model.DeviceConfigurationKeyNameTypePvCurtailmentLimitFactor + + deviceConfiguration, err := util.DeviceConfiguration(e.service, entity) + if err != nil || deviceConfiguration == nil { + return 0, err + } + + // check if device configuration description has curtailment limit factor key name + _, err = deviceConfiguration.GetDescriptionForKeyName(keyname) + if err != nil { + return 0, err + } + + data, err := deviceConfiguration.GetKeyValueForKeyName(keyname, model.DeviceConfigurationKeyValueTypeTypeScaledNumber) + if err != nil { + return 0, err + } + + if data == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + value, ok := data.(*model.ScaledNumberType) + if !ok || value == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + return value.GetValue(), nil +} + +// Scenario 2 + +// return the momentary power consumption or production at the grid connection point +// +// - positive values are used for consumption +// - negative values are used for production +func (e *UCMGCP) Power(entity spineapi.EntityRemoteInterface) (float64, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + + values, err := util.MeasurementValuesForTypeCommodityScope( + e.service, + entity, + model.MeasurementTypeTypePower, + model.CommodityTypeTypeElectricity, + model.ScopeTypeTypeACPowerTotal, + model.EnergyDirectionTypeConsume, + nil, + ) + if err != nil { + return 0, err + } + if len(values) != 1 { + return 0, eebusapi.ErrDataNotAvailable + } + + return values[0], nil +} + +// Scenario 3 + +// return the total feed in energy at the grid connection point +// +// - negative values are used for production +func (e *UCMGCP) EnergyFeedIn(entity spineapi.EntityRemoteInterface) (float64, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + + measurement := model.MeasurementTypeTypeEnergy + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeGridFeedIn + values, err := util.GetValuesForTypeCommodityScope(e.service, entity, measurement, commodity, scope) + if err != nil { + return 0, err + } + if len(values) == 0 { + return 0, eebusapi.ErrDataNotAvailable + } + + // we assume thre is only one result + value := values[0].Value + if value == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + return value.GetValue(), nil +} + +// Scenario 4 + +// return the total consumption energy at the grid connection point +// +// - positive values are used for consumption +func (e *UCMGCP) EnergyConsumed(entity spineapi.EntityRemoteInterface) (float64, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + + measurement := model.MeasurementTypeTypeEnergy + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeGridConsumption + values, err := util.GetValuesForTypeCommodityScope(e.service, entity, measurement, commodity, scope) + if err != nil { + return 0, err + } + if len(values) == 0 { + return 0, eebusapi.ErrDataNotAvailable + } + + // we assume thre is only one result + value := values[0].Value + if value == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + return value.GetValue(), nil +} + +// Scenario 5 + +// return the momentary current consumption or production at the grid connection point +// +// - positive values are used for consumption +// - negative values are used for production +func (e *UCMGCP) CurrentPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return nil, api.ErrNoCompatibleEntity + } + + return util.MeasurementValuesForTypeCommodityScope( + e.service, + entity, + model.MeasurementTypeTypeCurrent, + model.CommodityTypeTypeElectricity, + model.ScopeTypeTypeACCurrent, + model.EnergyDirectionTypeConsume, + util.PhaseNameMapping, + ) +} + +// Scenario 6 + +// return the voltage phase details at the grid connection point +func (e *UCMGCP) VoltagePerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return nil, api.ErrNoCompatibleEntity + } + + return util.MeasurementValuesForTypeCommodityScope( + e.service, + entity, + model.MeasurementTypeTypeVoltage, + model.CommodityTypeTypeElectricity, + model.ScopeTypeTypeACVoltage, + "", + util.PhaseNameMapping, + ) +} + +// Scenario 7 + +// return frequency at the grid connection point +func (e *UCMGCP) Frequency(entity spineapi.EntityRemoteInterface) (float64, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + + measurement := model.MeasurementTypeTypeFrequency + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeACFrequency + values, err := util.GetValuesForTypeCommodityScope(e.service, entity, measurement, commodity, scope) + if err != nil { + return 0, err + } + if len(values) == 0 { + return 0, eebusapi.ErrDataNotAvailable + } + + // take the first item + value := values[0].Value + if value == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + return value.GetValue(), nil +} diff --git a/ucmgcp/public_test.go b/ucmgcp/public_test.go new file mode 100644 index 0000000..7523fbc --- /dev/null +++ b/ucmgcp/public_test.go @@ -0,0 +1,463 @@ +package ucmgcp + +import ( + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCMGCPSuite) Test_PowerLimitationFactor() { + data, err := s.sut.PowerLimitationFactor(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.PowerLimitationFactor(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypePvCurtailmentLimitFactor), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerLimitationFactor(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(10), + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerLimitationFactor(s.smgwEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} + +func (s *UCMGCPSuite) Test_Power() { + data, err := s.sut.Power(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.Power(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypePower), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Power(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Power(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + elDescData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + PositiveEnergyDirection: eebusutil.Ptr(model.EnergyDirectionTypeConsume), + }, + }, + } + + rElFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionDescriptionListData, elDescData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Power(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + }, + }, + } + + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, elParamData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Power(s.smgwEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} + +func (s *UCMGCPSuite) Test_EnergyFeedIn() { + data, err := s.sut.EnergyFeedIn(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.EnergyFeedIn(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeGridFeedIn), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyFeedIn(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyFeedIn(s.smgwEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} + +func (s *UCMGCPSuite) Test_EnergyConsumed() { + data, err := s.sut.EnergyConsumed(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.EnergyConsumed(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeGridConsumption), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyConsumed(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyConsumed(s.smgwEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} + +func (s *UCMGCPSuite) Test_CurrentPerPhase() { + data, err := s.sut.CurrentPerPhase(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + data, err = s.sut.CurrentPerPhase(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypeCurrent), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACCurrent), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypeCurrent), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACCurrent), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypeCurrent), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACCurrent), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.CurrentPerPhase(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.CurrentPerPhase(s.smgwEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 0, len(data)) + + elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeC), + }, + }, + } + + rElFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, elParamData, nil, nil) + assert.Nil(s.T(), fErr) + + elDescData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + PositiveEnergyDirection: eebusutil.Ptr(model.EnergyDirectionTypeConsume), + }, + }, + } + + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionDescriptionListData, elDescData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.CurrentPerPhase(s.smgwEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), []float64{10, 10, 10}, data) +} + +func (s *UCMGCPSuite) Test_VoltagePerPhase() { + data, err := s.sut.VoltagePerPhase(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + data, err = s.sut.VoltagePerPhase(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypeVoltage), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACVoltage), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypeVoltage), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACVoltage), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypeVoltage), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACVoltage), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.VoltagePerPhase(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(230), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(230), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + Value: model.NewScaledNumberType(230), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.VoltagePerPhase(s.smgwEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 0, len(data)) + + elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeC), + }, + }, + } + + rElFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, elParamData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.VoltagePerPhase(s.smgwEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), []float64{230, 230, 230}, data) +} + +func (s *UCMGCPSuite) Test_Frequency() { + data, err := s.sut.Frequency(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.Frequency(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypeFrequency), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACFrequency), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Frequency(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(50), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Frequency(s.smgwEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 50.0, data) +} diff --git a/ucmgcp/testhelper_test.go b/ucmgcp/testhelper_test.go new file mode 100644 index 0000000..338ee77 --- /dev/null +++ b/ucmgcp/testhelper_test.go @@ -0,0 +1,174 @@ +package ucmgcp + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/cemd/api" + eebusapi "github.com/enbility/eebus-go/api" + eebusmocks "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestMGCPSuite(t *testing.T) { + suite.Run(t, new(UCMGCPSuite)) +} + +type UCMGCPSuite struct { + suite.Suite + + sut *UCMGCP + + service eebusapi.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *mocks.EntityRemoteInterface + smgwEntity spineapi.EntityRemoteInterface +} + +func (s *UCMGCPSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { +} + +func (s *UCMGCPSuite) BeforeTest(suiteName, testName string) { + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := eebusapi.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := eebusmocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := mocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = mocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := mocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + s.sut = NewUCMGCP(s.service, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + s.remoteDevice, s.smgwEntity = setupDevices(s.service, s.T()) +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService eebusapi.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeMeasurement, + []model.FunctionType{ + model.FunctionTypeMeasurementDescriptionListData, + model.FunctionTypeMeasurementConstraintsListData, + model.FunctionTypeMeasurementListData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + model.FunctionTypeElectricalConnectionDescriptionListData, + }, + }, + {model.FeatureTypeTypeDeviceConfiguration, + []model.FunctionType{ + model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, + model.FunctionTypeDeviceConfigurationKeyValueListData, + }, + }, + } + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: eebusutil.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + Feature: eebusutil.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: eebusutil.Ptr(feature.featureType), + Role: eebusutil.Ptr(model.RoleTypeServer), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeGridConnectionPointOfPremises), + }, + }, + }, + FeatureInformation: featureInformations, + } + + entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + remoteDevice.UpdateDevice(detailedData.DeviceInformation.Description) + + localDevice.AddRemoteDeviceForSki(remoteSki, remoteDevice) + + return remoteDevice, entities[0] +} diff --git a/ucmgcp/types.go b/ucmgcp/types.go new file mode 100644 index 0000000..3d453a3 --- /dev/null +++ b/ucmgcp/types.go @@ -0,0 +1,55 @@ +package ucmgcp + +import "github.com/enbility/cemd/api" + +const ( + // Grid maximum allowed feed-in power as percentage value of the cumulated + // nominal peak power of all electricity producting PV systems was updated + // + // Use `PowerLimitationFactor` to get the current data + // + // Use Case MGCP, Scenario 2 + DataUpdatePowerLimitationFactor api.EventType = "DataUpdatePowerLimitationFactor" + + // Grid momentary power consumption/production data updated + // + // Use `Power` to get the current data + // + // Use Case MGCP, Scenario 2 + DataUpdatePower api.EventType = "DataUpdatePower" + + // Total grid feed in energy data updated + // + // Use `EnergyFeedIn` to get the current data + // + // Use Case MGCP, Scenario 3 + DataUpdateEnergyFeedIn api.EventType = "DataUpdateEnergyFeedIn" + + // Total grid consumed energy data updated + // + // Use `EnergyConsumed` to get the current data + // + // Use Case MGCP, Scenario 4 + DataUpdateEnergyConsumed api.EventType = "DataUpdateEnergyConsumed" + + // Phase specific momentary current consumption/production phase detail data updated + // + // Use `CurrentPerPhase` to get the current data + // + // Use Case MGCP, Scenario 5 + DataUpdateCurrentPerPhase api.EventType = "DataUpdateCurrentPerPhase" + + // Phase specific voltage at the grid connection point + // + // Use `VoltagePerPhase` to get the current data + // + // Use Case MGCP, Scenario 6 + DataUpdateVoltagePerPhase api.EventType = "DataUpdateVoltagePerPhase" + + // Grid frequency data updated + // + // Use `Frequency` to get the current data + // + // Use Case MGCP, Scenario 7 + DataUpdateFrequency api.EventType = "DataUpdateFrequency" +) diff --git a/ucmgcp/ucmgcp.go b/ucmgcp/ucmgcp.go new file mode 100644 index 0000000..a4fe9be --- /dev/null +++ b/ucmgcp/ucmgcp.go @@ -0,0 +1,121 @@ +package ucmgcp + +import ( + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +type UCMGCP struct { + service eebusapi.ServiceInterface + + eventCB api.EntityEventCallback + + validEntityTypes []model.EntityTypeType +} + +var _ UCMGCPInterface = (*UCMGCP)(nil) + +func NewUCMGCP(service eebusapi.ServiceInterface, eventCB api.EntityEventCallback) *UCMGCP { + uc := &UCMGCP{ + service: service, + eventCB: eventCB, + } + + uc.validEntityTypes = []model.EntityTypeType{ + model.EntityTypeTypeCEM, + model.EntityTypeTypeGridConnectionPointOfPremises, + } + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (c *UCMGCP) UseCaseName() model.UseCaseNameType { + return model.UseCaseNameTypeMonitoringOfGridConnectionPoint +} + +func (e *UCMGCP) AddFeatures() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceConfiguration, + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + } + for _, feature := range clientFeatures { + _ = localEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } +} + +func (e *UCMGCP) AddUseCase() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.AddUseCaseSupport( + model.UseCaseActorTypeMonitoringAppliance, + e.UseCaseName(), + model.SpecificationVersionType("1.0.0"), + "release", + true, + []model.UseCaseScenarioSupportType{1, 2, 3, 4, 5, 6, 7}) +} + +func (e *UCMGCP) UpdateUseCaseAvailability(available bool) { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.SetUseCaseAvailability(model.UseCaseActorTypeMonitoringAppliance, e.UseCaseName(), available) +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *UCMGCP) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return false, api.ErrNoCompatibleEntity + } + + // check if the usecase and mandatory scenarios are supported and + // if the required server features are available + if !entity.Device().VerifyUseCaseScenariosAndFeaturesSupport( + model.UseCaseActorTypeGridConnectionPoint, + e.UseCaseName(), + []model.UseCaseScenarioSupportType{2, 3, 4}, + []model.FeatureTypeType{ + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + }, + ) { + return false, nil + } + + // check if measurement description contain data for the required scope + measurement, err := util.Measurement(e.service, entity) + if err != nil { + return false, eebusapi.ErrFunctionNotSupported + } + + _, err1 := measurement.GetDescriptionsForScope(model.ScopeTypeTypeACPower) + _, err2 := measurement.GetDescriptionsForScope(model.ScopeTypeTypeGridFeedIn) + _, err3 := measurement.GetDescriptionsForScope(model.ScopeTypeTypeGridConsumption) + if err1 != nil || err2 != nil || err3 != nil { + return false, eebusapi.ErrDataNotAvailable + } + + // check if electrical connection descriptions is provided + electricalConnection, err := util.ElectricalConnection(e.service, entity) + if err != nil { + return false, eebusapi.ErrFunctionNotSupported + } + + if _, err = electricalConnection.GetDescriptions(); err != nil { + return false, err + } + + return true, nil +} diff --git a/ucmgcp/ucmgcp_test.go b/ucmgcp/ucmgcp_test.go new file mode 100644 index 0000000..aae33cc --- /dev/null +++ b/ucmgcp/ucmgcp_test.go @@ -0,0 +1,86 @@ +package ucmgcp + +import ( + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCMGCPSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *UCMGCPSuite) Test_IsUseCaseSupported() { + data, err := s.sut.IsUseCaseSupported(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + data, err = s.sut.IsUseCaseSupported(s.smgwEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: eebusutil.Ptr(model.UseCaseActorTypeGridConnectionPoint), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: eebusutil.Ptr(model.UseCaseNameTypeMonitoringOfGridConnectionPoint), + UseCaseAvailable: eebusutil.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{2, 3, 4}, + }, + }, + }, + }, + } + + nodemgmtEntity := s.remoteDevice.Entity([]model.AddressEntityType{0}) + nodeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(nodemgmtEntity, model.FeatureTypeTypeNodeManagement, model.RoleTypeSpecial) + fErr := nodeFeature.UpdateData(model.FunctionTypeNodeManagementUseCaseData, ucData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACPower), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeGridFeedIn), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeGridConsumption), + }, + }, + } + + measurementFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr = measurementFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + elData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + }, + }, + } + + elFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = elFeature.UpdateData(model.FunctionTypeElectricalConnectionDescriptionListData, elData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.smgwEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/ucmpc/api.go b/ucmpc/api.go new file mode 100644 index 0000000..87a8b4f --- /dev/null +++ b/ucmpc/api.go @@ -0,0 +1,82 @@ +package ucmpc + +import ( + "github.com/enbility/cemd/api" + spineapi "github.com/enbility/spine-go/api" +) + +//go:generate mockery + +// interface for the Monitoring of Power Consumption UseCase +type UCMCPInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // return the momentary active power consumption or production + // + // parameters: + // - entity: the entity of the device (e.g. EVSE) + // + // possible errors: + // - ErrDataNotAvailable if no such limit is (yet) available + // - and others + Power(entity spineapi.EntityRemoteInterface) (float64, error) + + // return the momentary active phase specific power consumption or production per phase + // + // parameters: + // - entity: the entity of the device (e.g. EVSE) + // + // possible errors: + // - ErrDataNotAvailable if no such limit is (yet) available + // - and others + PowerPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) + + // Scenario 2 + + // return the total consumption energy + // + // parameters: + // - entity: the entity of the device (e.g. EVSE) + // + // - positive values are used for consumption + EnergyConsumed(entity spineapi.EntityRemoteInterface) (float64, error) + + // return the total feed in energy + // + // parameters: + // - entity: the entity of the device (e.g. EVSE) + // + // return values: + // - negative values are used for production + EnergyProduced(entity spineapi.EntityRemoteInterface) (float64, error) + + // Scenario 3 + + // return the momentary phase specific current consumption or production + // + // parameters: + // - entity: the entity of the device (e.g. EVSE) + // + // return values + // - positive values are used for consumption + // - negative values are used for production + CurrentPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) + + // Scenario 4 + + // return the phase specific voltage details + // + // parameters: + // - entity: the entity of the device (e.g. EVSE) + VoltagePerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) + + // Scenario 5 + + // return frequency + // + // parameters: + // - entity: the entity of the device (e.g. EVSE) + Frequency(entity spineapi.EntityRemoteInterface) (float64, error) +} diff --git a/ucmpc/events.go b/ucmpc/events.go new file mode 100644 index 0000000..36ffa9a --- /dev/null +++ b/ucmpc/events.go @@ -0,0 +1,113 @@ +package ucmpc + +import ( + "github.com/enbility/cemd/util" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// handle SPINE events +func (e *UCMPC) HandleEvent(payload spineapi.EventPayload) { + // only about events from an SGMW entity or device changes for this remote device + + if !util.IsCompatibleEntity(payload.Entity, e.validEntityTypes) { + return + } + + if util.IsEntityConnected(payload) { + e.deviceConnected(payload.Entity) + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + + switch payload.Data.(type) { + case *model.MeasurementDescriptionListDataType: + e.deviceMeasurementDescriptionDataUpdate(payload.Entity) + case *model.MeasurementListDataType: + e.deviceMeasurementDataUpdate(payload) + } +} + +// process required steps when a device is connected +func (e *UCMPC) deviceConnected(entity spineapi.EntityRemoteInterface) { + if electricalConnection, err := util.ElectricalConnection(e.service, entity); err == nil { + if _, err := electricalConnection.Subscribe(); err != nil { + logging.Log().Error(err) + } + + // get electrical connection parameter + if _, err := electricalConnection.RequestDescriptions(); err != nil { + logging.Log().Error(err) + } + + if _, err := electricalConnection.RequestParameterDescriptions(); err != nil { + logging.Log().Error(err) + } + } + + if measurement, err := util.Measurement(e.service, entity); err == nil { + if _, err := measurement.Subscribe(); err != nil { + logging.Log().Error(err) + } + + // get measurement parameters + if _, err := measurement.RequestDescriptions(); err != nil { + logging.Log().Error(err) + } + + if _, err := measurement.RequestConstraints(); err != nil { + logging.Log().Error(err) + } + } +} + +// the measurement descriptiondata of a device was updated +func (e *UCMPC) deviceMeasurementDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if measurement, err := util.Measurement(e.service, entity); err == nil { + // measurement descriptions received, now get the data + if _, err := measurement.RequestValues(); err != nil { + logging.Log().Error("Error getting measurement list values:", err) + } + } +} + +// the measurement data of a device was updated +func (e *UCMPC) deviceMeasurementDataUpdate(payload spineapi.EventPayload) { + // Scenario 1 + if util.MeasurementCheckPayloadDataForScope(e.service, payload, model.ScopeTypeTypeACPowerTotal) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePower) + } + + if util.MeasurementCheckPayloadDataForScope(e.service, payload, model.ScopeTypeTypeACPower) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePowerPerPhase) + } + + // Scenario 2 + if util.MeasurementCheckPayloadDataForScope(e.service, payload, model.ScopeTypeTypeACEnergyConsumed) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateEnergyConsumed) + } + + if util.MeasurementCheckPayloadDataForScope(e.service, payload, model.ScopeTypeTypeACEnergyProduced) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateEnergyProduced) + } + + // Scenario 3 + if util.MeasurementCheckPayloadDataForScope(e.service, payload, model.ScopeTypeTypeACCurrent) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateCurrentsPerPhase) + } + + // Scenario 4 + if util.MeasurementCheckPayloadDataForScope(e.service, payload, model.ScopeTypeTypeACVoltage) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateVoltagePerPhase) + } + + // Scenario 5 + if util.MeasurementCheckPayloadDataForScope(e.service, payload, model.ScopeTypeTypeACFrequency) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateFrequency) + } +} diff --git a/ucmpc/events_test.go b/ucmpc/events_test.go new file mode 100644 index 0000000..9a98f78 --- /dev/null +++ b/ucmpc/events_test.go @@ -0,0 +1,125 @@ +package ucmpc + +import ( + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCMPCSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.monitoredEntity + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeEntityChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.Data = eebusutil.Ptr(model.MeasurementDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.MeasurementListDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *UCMPCSuite) Test_Failures() { + s.sut.deviceConnected(s.mockRemoteEntity) + + s.sut.deviceMeasurementDescriptionDataUpdate(s.mockRemoteEntity) +} + +func (s *UCMPCSuite) Test_deviceMeasurementDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.monitoredEntity, + } + s.sut.deviceMeasurementDataUpdate(payload) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACPower), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACEnergyConsumed), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(3)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACEnergyProduced), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(4)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACCurrent), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(5)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACVoltage), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(6)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACFrequency), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.deviceMeasurementDataUpdate(payload) + + data := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(3)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(4)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(5)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(6)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + payload.Data = data + + s.sut.deviceMeasurementDataUpdate(payload) +} diff --git a/ucmpc/public.go b/ucmpc/public.go new file mode 100644 index 0000000..5f96ed3 --- /dev/null +++ b/ucmpc/public.go @@ -0,0 +1,189 @@ +package ucmpc + +import ( + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// Scenario 1 + +// return the momentary active power consumption or production +// +// possible errors: +// - ErrDataNotAvailable if no such limit is (yet) available +// - and others +func (e *UCMPC) Power(entity spineapi.EntityRemoteInterface) (float64, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + + values, err := util.MeasurementValuesForTypeCommodityScope( + e.service, + entity, + model.MeasurementTypeTypePower, + model.CommodityTypeTypeElectricity, + model.ScopeTypeTypeACPowerTotal, + model.EnergyDirectionTypeConsume, + nil, + ) + if err != nil { + return 0, err + } + if len(values) != 1 { + return 0, eebusapi.ErrDataNotAvailable + } + return values[0], nil +} + +// return the momentary active phase specific power consumption or production per phase +// +// possible errors: +// - ErrDataNotAvailable if no such limit is (yet) available +// - and others +func (e *UCMPC) PowerPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return nil, api.ErrNoCompatibleEntity + } + + return util.MeasurementValuesForTypeCommodityScope( + e.service, + entity, + model.MeasurementTypeTypePower, + model.CommodityTypeTypeElectricity, + model.ScopeTypeTypeACPower, + model.EnergyDirectionTypeConsume, + util.PhaseNameMapping, + ) +} + +// Scenario 2 + +// return the total consumption energy +// +// - positive values are used for consumption +func (e *UCMPC) EnergyConsumed(entity spineapi.EntityRemoteInterface) (float64, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + + measurement := model.MeasurementTypeTypeEnergy + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeACEnergyConsumed + values, err := util.GetValuesForTypeCommodityScope(e.service, entity, measurement, commodity, scope) + if err != nil { + return 0, err + } + if len(values) == 0 { + return 0, eebusapi.ErrDataNotAvailable + } + + // we assume thre is only one result + value := values[0].Value + if value == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + return value.GetValue(), nil +} + +// return the total feed in energy +// +// - negative values are used for production +func (e *UCMPC) EnergyProduced(entity spineapi.EntityRemoteInterface) (float64, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + + measurement := model.MeasurementTypeTypeEnergy + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeACEnergyProduced + values, err := util.GetValuesForTypeCommodityScope(e.service, entity, measurement, commodity, scope) + if err != nil { + return 0, err + } + if len(values) == 0 { + return 0, eebusapi.ErrDataNotAvailable + } + + // we assume thre is only one result + value := values[0].Value + if value == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + return value.GetValue(), nil +} + +// Scenario 3 + +// return the momentary phase specific current consumption or production +// +// - positive values are used for consumption +// - negative values are used for production +func (e *UCMPC) CurrentPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return nil, api.ErrNoCompatibleEntity + } + + return util.MeasurementValuesForTypeCommodityScope( + e.service, + entity, + model.MeasurementTypeTypeCurrent, + model.CommodityTypeTypeElectricity, + model.ScopeTypeTypeACCurrent, + model.EnergyDirectionTypeConsume, + util.PhaseNameMapping, + ) +} + +// Scenario 4 + +// return the phase specific voltage details +func (e *UCMPC) VoltagePerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return nil, api.ErrNoCompatibleEntity + } + + return util.MeasurementValuesForTypeCommodityScope( + e.service, + entity, + model.MeasurementTypeTypeVoltage, + model.CommodityTypeTypeElectricity, + model.ScopeTypeTypeACVoltage, + "", + util.PhaseNameMapping, + ) +} + +// Scenario 5 + +// return frequency +func (e *UCMPC) Frequency(entity spineapi.EntityRemoteInterface) (float64, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + + measurement := model.MeasurementTypeTypeFrequency + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeACFrequency + values, err := util.GetValuesForTypeCommodityScope(e.service, entity, measurement, commodity, scope) + + if err != nil { + return 0, err + } + if len(values) == 0 { + return 0, eebusapi.ErrDataNotAvailable + } + + // take the first item + value := values[0].Value + + if value == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + return value.GetValue(), nil +} diff --git a/ucmpc/public_test.go b/ucmpc/public_test.go new file mode 100644 index 0000000..fc81c93 --- /dev/null +++ b/ucmpc/public_test.go @@ -0,0 +1,523 @@ +package ucmpc + +import ( + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCMPCSuite) Test_Power() { + data, err := s.sut.Power(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.Power(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypePower), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Power(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Power(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + elDescData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + PositiveEnergyDirection: eebusutil.Ptr(model.EnergyDirectionTypeConsume), + }, + }, + } + + rElFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionDescriptionListData, elDescData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Power(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + }, + }, + } + + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, elParamData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Power(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} + +func (s *UCMPCSuite) Test_PowerPerPhase() { + data, err := s.sut.PowerPerPhase(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + data, err = s.sut.PowerPerPhase(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypePower), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACPower), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypePower), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACPower), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypePower), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACPower), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerPerPhase(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerPerPhase(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 0, len(data)) + + elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeC), + }, + }, + } + + rElFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, elParamData, nil, nil) + assert.Nil(s.T(), fErr) + + elDescData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + PositiveEnergyDirection: eebusutil.Ptr(model.EnergyDirectionTypeConsume), + }, + }, + } + + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionDescriptionListData, elDescData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerPerPhase(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), []float64{10, 10, 10}, data) +} + +func (s *UCMPCSuite) Test_EnergyConsumed() { + data, err := s.sut.EnergyConsumed(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.EnergyConsumed(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACEnergyConsumed), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyConsumed(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyConsumed(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} + +func (s *UCMPCSuite) Test_EnergyProduced() { + data, err := s.sut.EnergyProduced(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.EnergyProduced(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACEnergyProduced), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyProduced(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyProduced(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} + +func (s *UCMPCSuite) Test_CurrentPerPhase() { + data, err := s.sut.CurrentPerPhase(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + data, err = s.sut.CurrentPerPhase(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypeCurrent), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACCurrent), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypeCurrent), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACCurrent), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypeCurrent), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACCurrent), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.CurrentPerPhase(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.CurrentPerPhase(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 0, len(data)) + + elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeC), + }, + }, + } + + rElFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, elParamData, nil, nil) + assert.Nil(s.T(), fErr) + + elDescData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + PositiveEnergyDirection: eebusutil.Ptr(model.EnergyDirectionTypeConsume), + }, + }, + } + + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionDescriptionListData, elDescData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.CurrentPerPhase(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), []float64{10, 10, 10}, data) +} + +func (s *UCMPCSuite) Test_VoltagePerPhase() { + data, err := s.sut.VoltagePerPhase(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + data, err = s.sut.VoltagePerPhase(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypeVoltage), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACVoltage), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypeVoltage), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACVoltage), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypeVoltage), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACVoltage), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.VoltagePerPhase(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(230), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(230), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + Value: model.NewScaledNumberType(230), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.VoltagePerPhase(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 0, len(data)) + + elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeC), + }, + }, + } + + rElFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, elParamData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.VoltagePerPhase(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), []float64{230, 230, 230}, data) +} + +func (s *UCMPCSuite) Test_Frequency() { + data, err := s.sut.Frequency(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.Frequency(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypeFrequency), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACFrequency), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Frequency(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(50), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Frequency(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 50.0, data) +} diff --git a/ucmpc/testhelper_test.go b/ucmpc/testhelper_test.go new file mode 100644 index 0000000..a26e07e --- /dev/null +++ b/ucmpc/testhelper_test.go @@ -0,0 +1,168 @@ +package ucmpc + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/cemd/api" + eebusapi "github.com/enbility/eebus-go/api" + eebusmocks "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestMPCSuite(t *testing.T) { + suite.Run(t, new(UCMPCSuite)) +} + +type UCMPCSuite struct { + suite.Suite + + sut *UCMPC + + service eebusapi.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *mocks.EntityRemoteInterface + monitoredEntity spineapi.EntityRemoteInterface +} + +func (s *UCMPCSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { +} + +func (s *UCMPCSuite) BeforeTest(suiteName, testName string) { + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := eebusapi.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := eebusmocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := mocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = mocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := mocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + s.sut = NewUCMPC(s.service, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + s.remoteDevice, s.monitoredEntity = setupDevices(s.service, s.T()) +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService eebusapi.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeMeasurement, + []model.FunctionType{ + model.FunctionTypeMeasurementDescriptionListData, + model.FunctionTypeMeasurementConstraintsListData, + model.FunctionTypeMeasurementListData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + model.FunctionTypeElectricalConnectionDescriptionListData, + }, + }, + } + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: eebusutil.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + Feature: eebusutil.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: eebusutil.Ptr(feature.featureType), + Role: eebusutil.Ptr(model.RoleTypeServer), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeEVSE), + }, + }, + }, + FeatureInformation: featureInformations, + } + + entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + remoteDevice.UpdateDevice(detailedData.DeviceInformation.Description) + + localDevice.AddRemoteDeviceForSki(remoteSki, remoteDevice) + + return remoteDevice, entities[0] +} diff --git a/ucmpc/types.go b/ucmpc/types.go new file mode 100644 index 0000000..1c5e01a --- /dev/null +++ b/ucmpc/types.go @@ -0,0 +1,54 @@ +package ucmpc + +import "github.com/enbility/cemd/api" + +const ( + // Total momentary active power consumption or production + // + // Use `Power` to get the current data + // + // Use Case MCP, Scenario 1 + DataUpdatePower api.EventType = "DataUpdatePower" + + // Phase specific momentary active power consumption or production + // + // Use `PowerPerPhase` to get the current data + // + // Use Case MCP, Scenario 1 + DataUpdatePowerPerPhase api.EventType = "DataUpdatePowerPerPhase" + + // Total energy consumed + // + // Use `EnergyConsumed` to get the current data + // + // Use Case MCP, Scenario 2 + DataUpdateEnergyConsumed api.EventType = "DataUpdateEnergyConsumed" + + // Total energy produced + // + // Use `EnergyProduced` to get the current data + // + // Use Case MCP, Scenario 2 + DataUpdateEnergyProduced api.EventType = "DataUpdateEnergyProduced" + + // Phase specific momentary current consumption or production + // + // Use `CurrentPerPhase` to get the current data + // + // Use Case MCP, Scenario 3 + DataUpdateCurrentsPerPhase api.EventType = "DataUpdateCurrentsPerPhase" + + // Phase specific voltage + // + // Use `VoltagePerPhase` to get the current data + // + // Use Case MCP, Scenario 3 + DataUpdateVoltagePerPhase api.EventType = "DataUpdateVoltagePerPhase" + + // Power network frequency data updated + // + // Use `Frequency` to get the current data + // + // Use Case MCP, Scenario 3 + DataUpdateFrequency api.EventType = "DataUpdateFrequency" +) diff --git a/ucmpc/ucmcp.go b/ucmpc/ucmcp.go new file mode 100644 index 0000000..e32e3ca --- /dev/null +++ b/ucmpc/ucmcp.go @@ -0,0 +1,127 @@ +package ucmpc + +import ( + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +type UCMPC struct { + service eebusapi.ServiceInterface + + eventCB api.EntityEventCallback + + validEntityTypes []model.EntityTypeType +} + +var _ UCMCPInterface = (*UCMPC)(nil) + +func NewUCMPC(service eebusapi.ServiceInterface, eventCB api.EntityEventCallback) *UCMPC { + uc := &UCMPC{ + service: service, + eventCB: eventCB, + } + + uc.validEntityTypes = []model.EntityTypeType{ + model.EntityTypeTypeCompressor, + model.EntityTypeTypeElectricalImmersionHeater, + model.EntityTypeTypeEVSE, + model.EntityTypeTypeHeatPumpAppliance, + model.EntityTypeTypeInverter, + model.EntityTypeTypeSmartEnergyAppliance, + model.EntityTypeTypeSubMeterElectricity, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (c *UCMPC) UseCaseName() model.UseCaseNameType { + return model.UseCaseNameTypeMonitoringOfPowerConsumption +} + +func (e *UCMPC) AddFeatures() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + } + for _, feature := range clientFeatures { + _ = localEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } +} + +func (e *UCMPC) AddUseCase() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.AddUseCaseSupport( + model.UseCaseActorTypeMonitoringAppliance, + e.UseCaseName(), + model.SpecificationVersionType("1.0.0"), + "release", + true, + []model.UseCaseScenarioSupportType{1, 2, 3, 4, 5}) +} + +func (e *UCMPC) UpdateUseCaseAvailability(available bool) { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.SetUseCaseAvailability(model.UseCaseActorTypeMonitoringAppliance, e.UseCaseName(), available) +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *UCMPC) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return false, api.ErrNoCompatibleEntity + } + + // check if the usecase and mandatory scenarios are supported and + // if the required server features are available + if !entity.Device().VerifyUseCaseScenariosAndFeaturesSupport( + model.UseCaseActorTypeMonitoredUnit, + e.UseCaseName(), + []model.UseCaseScenarioSupportType{1}, + []model.FeatureTypeType{ + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + }, + ) { + return false, nil + } + + // check if measurement description contain data for the required scope + measurement, err := util.Measurement(e.service, entity) + if err != nil { + return false, eebusapi.ErrFunctionNotSupported + } + + if _, err := measurement.GetDescriptionsForScope(model.ScopeTypeTypeACPowerTotal); err != nil { + return false, eebusapi.ErrDataNotAvailable + } + + // check if electrical connection descriptions is provided + electricalConnection, err := util.ElectricalConnection(e.service, entity) + if err != nil { + return false, eebusapi.ErrFunctionNotSupported + } + + if _, err = electricalConnection.GetDescriptions(); err != nil { + return false, err + } + + if _, err = electricalConnection.GetParameterDescriptions(); err != nil { + return false, err + } + + return true, nil +} diff --git a/ucmpc/ucmcp_test.go b/ucmpc/ucmcp_test.go new file mode 100644 index 0000000..02ff864 --- /dev/null +++ b/ucmpc/ucmcp_test.go @@ -0,0 +1,93 @@ +package ucmpc + +import ( + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCMPCSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *UCMPCSuite) Test_IsUseCaseSupported() { + data, err := s.sut.IsUseCaseSupported(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + data, err = s.sut.IsUseCaseSupported(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: eebusutil.Ptr(model.UseCaseActorTypeMonitoredUnit), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: eebusutil.Ptr(model.UseCaseNameTypeMonitoringOfPowerConsumption), + UseCaseAvailable: eebusutil.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1}, + }, + }, + }, + }, + } + + nodemgmtEntity := s.remoteDevice.Entity([]model.AddressEntityType{0}) + nodeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(nodemgmtEntity, model.FeatureTypeTypeNodeManagement, model.RoleTypeSpecial) + fErr := nodeFeature.UpdateData(model.FunctionTypeNodeManagementUseCaseData, ucData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + }, + } + + measurementFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr = measurementFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + elData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + }, + }, + } + + elFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = elFeature.UpdateData(model.FunctionTypeElectricalConnectionDescriptionListData, elData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + paramData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + }, + }, + } + + fErr = elFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/ucopev/api.go b/ucopev/api.go new file mode 100644 index 0000000..6c6d81d --- /dev/null +++ b/ucopev/api.go @@ -0,0 +1,65 @@ +package ucopev + +import ( + "github.com/enbility/cemd/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +//go:generate mockery + +// interface for the Overload Protection by EV Charging Current Curtailment UseCase +type UCOPEVInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // return the min, max, default limits for each phase of the connected EV + // + // parameters: + // - entity: the entity of the EV + CurrentLimits(entity spineapi.EntityRemoteInterface) ([]float64, []float64, []float64, error) + + // return the current loadcontrol obligation limits + // + // parameters: + // - entity: the entity of the EV + // + // return values: + // - limits: per phase data + // + // possible errors: + // - ErrDataNotAvailable if no such limit is (yet) available + // - and others + LoadControlLimits(entity spineapi.EntityRemoteInterface) (limits []api.LoadLimitsPhase, resultErr error) + + // send new LoadControlLimits to the remote EV + // + // parameters: + // - entity: the entity of the EV + // - limits: a set of limits containing phase specific limit data + // + // Sets a maximum A limit for each phase that the EV may not exceed. + // Mainly used for implementing overload protection of the site or limiting the + // maximum charge power of EVs when the EV and EVSE communicate via IEC61851 + // and with ISO15118 if the EV does not support the Optimization of Self Consumption + // usecase. + // + // 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. + WriteLoadControlLimits(entity spineapi.EntityRemoteInterface, limits []api.LoadLimitsPhase) (*model.MsgCounterType, error) + + // Scenario 2 + + // this is automatically covered by the SPINE implementation + + // Scenario 3 + + // this is covered by the central CEM interface implementation + // use that one to set the CEM's operation state which will inform all remote devices +} diff --git a/ucopev/events.go b/ucopev/events.go new file mode 100644 index 0000000..369a02f --- /dev/null +++ b/ucopev/events.go @@ -0,0 +1,100 @@ +package ucopev + +import ( + "github.com/enbility/cemd/util" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// handle SPINE events +func (e *UCOPEV) HandleEvent(payload spineapi.EventPayload) { + // only about events from an EV entity or device changes for this remote device + + if !util.IsCompatibleEntity(payload.Entity, e.validEntityTypes) { + return + } + + if util.IsEntityConnected(payload) { + e.evConnected(payload.Entity) + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + + switch payload.Data.(type) { + case *model.ElectricalConnectionPermittedValueSetListDataType: + e.evElectricalPermittedValuesUpdate(payload) + case *model.LoadControlLimitDescriptionListDataType: + e.evLoadControlLimitDescriptionDataUpdate(payload.Entity) + case *model.LoadControlLimitListDataType: + e.evLoadControlLimitDataUpdate(payload) + } +} + +// an EV was connected +func (e *UCOPEV) evConnected(entity spineapi.EntityRemoteInterface) { + // initialise features, e.g. subscriptions, descriptions + if evLoadControl, err := util.LoadControl(e.service, entity); err == nil { + if _, err := evLoadControl.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + if _, err := evLoadControl.Bind(); err != nil { + logging.Log().Debug(err) + } + + // get descriptions + if _, err := evLoadControl.RequestLimitDescriptions(); err != nil { + logging.Log().Debug(err) + } + + // get constraints + if _, err := evLoadControl.RequestLimitConstraints(); err != nil { + logging.Log().Debug(err) + } + } +} + +// the load control limit description data of an EV was updated +func (e *UCOPEV) evLoadControlLimitDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if evLoadControl, err := util.LoadControl(e.service, entity); err == nil { + // get values + if _, err := evLoadControl.RequestLimitValues(); err != nil { + logging.Log().Debug(err) + } + } +} + +// the load control limit data of an EV was updated +func (e *UCOPEV) evLoadControlLimitDataUpdate(payload spineapi.EventPayload) { + if util.LoadControlLimitsCheckPayloadDataForTypeCategoryDirectionScope(false, + e.service, payload, model.LoadControlLimitTypeTypeMaxValueLimit, + model.LoadControlCategoryTypeObligation, "", model.ScopeTypeTypeOverloadProtection) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateLimit) + } +} + +// the electrical connection permitted value sets data of an EV was updated +func (e *UCOPEV) evElectricalPermittedValuesUpdate(payload spineapi.EventPayload) { + evElectricalConnection, err := util.ElectricalConnection(e.service, payload.Entity) + if err != nil { + return + } + + data, err := evElectricalConnection.GetParameterDescriptionForMeasuredPhase(model.ElectricalConnectionPhaseNameTypeA) + if err != nil || data.ParameterId == nil { + return + } + + values, err := evElectricalConnection.GetPermittedValueSetForParameterId(*data.ParameterId) + if err != nil || values == nil { + return + } + + // Scenario 6 + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateCurrentLimits) +} diff --git a/ucopev/events_test.go b/ucopev/events_test.go new file mode 100644 index 0000000..534b7c4 --- /dev/null +++ b/ucopev/events_test.go @@ -0,0 +1,169 @@ +package ucopev + +import ( + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCOPEVSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.evEntity + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeEntityChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.Data = eebusutil.Ptr(model.ElectricalConnectionPermittedValueSetListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.LoadControlLimitDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.LoadControlLimitListDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *UCOPEVSuite) Test_Failures() { + s.sut.evConnected(s.mockRemoteEntity) + + s.sut.evLoadControlLimitDescriptionDataUpdate(s.mockRemoteEntity) +} + +func (s *UCOPEVSuite) Test_evElectricalPermittedValuesUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evElectricalPermittedValuesUpdate(payload) + + payload.Entity = s.evEntity + s.sut.evElectricalPermittedValuesUpdate(payload) + + paramData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(1)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(2)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeC), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evElectricalPermittedValuesUpdate(payload) + + data := &model.ElectricalConnectionPermittedValueSetListDataType{ + ElectricalConnectionPermittedValueSetData: []model.ElectricalConnectionPermittedValueSetDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(0)), + PermittedValueSet: []model.ScaledNumberSetType{ + { + Value: []model.ScaledNumberType{ + *model.NewScaledNumberType(0.1), + }, + Range: []model.ScaledNumberRangeType{ + { + Min: model.NewScaledNumberType(1400), + Max: model.NewScaledNumberType(11000), + }, + }, + }, + }, + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(1)), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(2)), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeElectricalConnectionPermittedValueSetListData, data, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evElectricalPermittedValuesUpdate(payload) +} + +func (s *UCOPEVSuite) Test_evLoadControlLimitDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evLoadControlLimitDataUpdate(payload) + + payload.Entity = s.evEntity + s.sut.evLoadControlLimitDataUpdate(payload) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + LimitType: eebusutil.Ptr(model.LoadControlLimitTypeTypeMaxValueLimit), + LimitCategory: eebusutil.Ptr(model.LoadControlCategoryTypeObligation), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeOverloadProtection), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeLoadControlLimitDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evLoadControlLimitDataUpdate(payload) + + data := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{}, + } + + payload.Data = data + + s.sut.evLoadControlLimitDataUpdate(payload) + + data = &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + Value: model.NewScaledNumberType(16), + }, + }, + } + + payload.Data = data + + s.sut.evLoadControlLimitDataUpdate(payload) +} diff --git a/ucopev/public.go b/ucopev/public.go new file mode 100644 index 0000000..84f6750 --- /dev/null +++ b/ucopev/public.go @@ -0,0 +1,61 @@ +package ucopev + +import ( + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// return the min, max, default limits for each phase of the connected EV +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (e *UCOPEV) CurrentLimits(entity spineapi.EntityRemoteInterface) ([]float64, []float64, []float64, error) { + return util.GetPhaseCurrentLimits(e.service, entity, e.validEntityTypes) +} + +// return the current loadcontrol obligation limits +// +// parameters: +// - entity: the entity of the EV +// +// return values: +// - limits: per phase data +// +// possible errors: +// - ErrDataNotAvailable if no such limit is (yet) available +// - and others +func (e *UCOPEV) LoadControlLimits(entity spineapi.EntityRemoteInterface) ( + limits []api.LoadLimitsPhase, resultErr error) { + return util.LoadControlLimits( + e.service, + entity, + e.validEntityTypes, + model.LoadControlLimitTypeTypeMaxValueLimit, + model.LoadControlCategoryTypeObligation, + model.ScopeTypeTypeOverloadProtection) +} + +// send new LoadControlLimits to the remote EV +// +// parameters: +// - limits: a set of limits containing phase specific limit data +// +// Sets a maximum A limit for each phase that the EV may not exceed. +// Mainly used for implementing overload protection of the site or limiting the +// maximum charge power of EVs when the EV and EVSE communicate via IEC61851 +// and with ISO15118 if the EV does not support the Optimization of Self Consumption +// usecase. +// +// 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. +func (e *UCOPEV) WriteLoadControlLimits(entity spineapi.EntityRemoteInterface, limits []api.LoadLimitsPhase) (*model.MsgCounterType, error) { + return util.WriteLoadControlLimits(e.service, entity, e.validEntityTypes, model.LoadControlCategoryTypeObligation, limits) +} diff --git a/ucopev/public_test.go b/ucopev/public_test.go new file mode 100644 index 0000000..cedcf8f --- /dev/null +++ b/ucopev/public_test.go @@ -0,0 +1,28 @@ +package ucopev + +import ( + "github.com/enbility/cemd/api" + "github.com/stretchr/testify/assert" +) + +func (s *UCOPEVSuite) Test_Public() { + // The actual tests of the functionality is located in the util package + + _, _, _, err := s.sut.CurrentLimits(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, _, _, err = s.sut.CurrentLimits(s.evEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.LoadControlLimits(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.LoadControlLimits(s.evEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.WriteLoadControlLimits(s.mockRemoteEntity, []api.LoadLimitsPhase{}) + assert.NotNil(s.T(), err) + + _, err = s.sut.WriteLoadControlLimits(s.evEntity, []api.LoadLimitsPhase{}) + assert.NotNil(s.T(), err) +} diff --git a/ucopev/testhelper_test.go b/ucopev/testhelper_test.go new file mode 100644 index 0000000..513ff90 --- /dev/null +++ b/ucopev/testhelper_test.go @@ -0,0 +1,185 @@ +package ucopev + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/cemd/api" + eebusapi "github.com/enbility/eebus-go/api" + eebusmocks "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestUCOPEVSuite(t *testing.T) { + suite.Run(t, new(UCOPEVSuite)) +} + +type UCOPEVSuite struct { + suite.Suite + + sut *UCOPEV + + service eebusapi.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *mocks.EntityRemoteInterface + evEntity spineapi.EntityRemoteInterface +} + +func (s *UCOPEVSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { +} + +func (s *UCOPEVSuite) BeforeTest(suiteName, testName string) { + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := eebusapi.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := eebusmocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := mocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = mocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := mocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + s.sut = NewUCOPEV(s.service, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + var entities []spineapi.EntityRemoteInterface + s.remoteDevice, entities = setupDevices(s.service, s.T()) + s.evEntity = entities[1] +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService eebusapi.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + []spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeElectricalConnection, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + model.FunctionTypeElectricalConnectionPermittedValueSetListData, + }, + }, + {model.FeatureTypeTypeLoadControl, + []model.FunctionType{ + model.FunctionTypeLoadControlLimitDescriptionListData, + model.FunctionTypeLoadControlLimitConstraintsListData, + model.FunctionTypeLoadControlLimitListData, + }, + }, + {model.FeatureTypeTypeDeviceDiagnosis, + []model.FunctionType{ + model.FunctionTypeDeviceDiagnosisStateData, + }, + }, + } + + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: eebusutil.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + Feature: eebusutil.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: eebusutil.Ptr(feature.featureType), + Role: eebusutil.Ptr(model.RoleTypeServer), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeEVSE), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeEV), + }, + }, + }, + FeatureInformation: featureInformations, + } + + entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + remoteDevice.UpdateDevice(detailedData.DeviceInformation.Description) + + localDevice.AddRemoteDeviceForSki(remoteSki, remoteDevice) + + return remoteDevice, entities +} diff --git a/ucopev/types.go b/ucopev/types.go new file mode 100644 index 0000000..32eebe6 --- /dev/null +++ b/ucopev/types.go @@ -0,0 +1,15 @@ +package ucopev + +import "github.com/enbility/cemd/api" + +const ( + // EV current limits + // + // Use `CurrentLimits` to get the current data + DataUpdateCurrentLimits api.EventType = "DataUpdateCurrentLimits" + + // EV load control obligation limit data updated + // + // Use `LoadControlLimits` to get the current data + DataUpdateLimit api.EventType = "DataUpdateLimit" +) diff --git a/ucopev/ucopev.go b/ucopev/ucopev.go new file mode 100644 index 0000000..e9c89ce --- /dev/null +++ b/ucopev/ucopev.go @@ -0,0 +1,110 @@ +package ucopev + +import ( + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +type UCOPEV struct { + service eebusapi.ServiceInterface + + eventCB api.EntityEventCallback + + validEntityTypes []model.EntityTypeType +} + +var _ UCOPEVInterface = (*UCOPEV)(nil) + +func NewUCOPEV(service eebusapi.ServiceInterface, eventCB api.EntityEventCallback) *UCOPEV { + uc := &UCOPEV{ + service: service, + eventCB: eventCB, + } + + uc.validEntityTypes = []model.EntityTypeType{ + model.EntityTypeTypeEV, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (c *UCOPEV) UseCaseName() model.UseCaseNameType { + return model.UseCaseNameTypeOverloadProtectionByEVChargingCurrentCurtailment +} + +func (e *UCOPEV) AddFeatures() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeLoadControl, + model.FeatureTypeTypeElectricalConnection, + } + for _, feature := range clientFeatures { + _ = localEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } + + // server features + f := localEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeDeviceDiagnosisStateData, true, false) + f.AddFunctionType(model.FunctionTypeDeviceDiagnosisHeartbeatData, true, false) +} + +func (e *UCOPEV) AddUseCase() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.AddUseCaseSupport( + model.UseCaseActorTypeCEM, + e.UseCaseName(), + model.SpecificationVersionType("1.0.1"), + "release", + true, + []model.UseCaseScenarioSupportType{1, 2, 3}) +} + +func (e *UCOPEV) UpdateUseCaseAvailability(available bool) { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.SetUseCaseAvailability(model.UseCaseActorTypeCEM, e.UseCaseName(), available) +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *UCOPEV) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return false, api.ErrNoCompatibleEntity + } + + // check if the usecase and mandatory scenarios are supported and + // if the required server features are available + if !entity.Device().VerifyUseCaseScenariosAndFeaturesSupport( + model.UseCaseActorTypeEV, + e.UseCaseName(), + []model.UseCaseScenarioSupportType{1, 2, 3}, + []model.FeatureTypeType{model.FeatureTypeTypeLoadControl}, + ) { + return false, nil + } + + // check for required features + evLoadControl, err := util.LoadControl(e.service, entity) + if err != nil { + return false, eebusapi.ErrFunctionNotSupported + } + + // check if loadcontrol limit descriptions contains a recommendation category + if _, err = evLoadControl.GetLimitDescriptionsForCategory(model.LoadControlCategoryTypeObligation); err != nil { + return false, err + } + + return true, nil +} diff --git a/ucopev/ucopev_test.go b/ucopev/ucopev_test.go new file mode 100644 index 0000000..26905a2 --- /dev/null +++ b/ucopev/ucopev_test.go @@ -0,0 +1,62 @@ +package ucopev + +import ( + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCOPEVSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *UCOPEVSuite) Test_IsUseCaseSupported() { + data, err := s.sut.IsUseCaseSupported(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + data, err = s.sut.IsUseCaseSupported(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: eebusutil.Ptr(model.UseCaseActorTypeEV), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: eebusutil.Ptr(model.UseCaseNameTypeOverloadProtectionByEVChargingCurrentCurtailment), + UseCaseAvailable: eebusutil.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1, 2, 3}, + }, + }, + }, + }, + } + + nodemgmtEntity := s.remoteDevice.Entity([]model.AddressEntityType{0}) + nodeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(nodemgmtEntity, model.FeatureTypeTypeNodeManagement, model.RoleTypeSpecial) + fErr := nodeFeature.UpdateData(model.FunctionTypeNodeManagementUseCaseData, ucData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + LimitCategory: eebusutil.Ptr(model.LoadControlCategoryTypeObligation), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + fErr = rFeature.UpdateData(model.FunctionTypeLoadControlLimitDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/ucoscev/api.go b/ucoscev/api.go new file mode 100644 index 0000000..13ac02e --- /dev/null +++ b/ucoscev/api.go @@ -0,0 +1,58 @@ +package ucoscev + +import ( + "github.com/enbility/cemd/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +//go:generate mockery + +// interface for the Optimization of Self-Consumption During EV Charging UseCase +type UCOSCEVInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // return the min, max, default limits for each phase of the connected EV + // + // parameters: + // - entity: the entity of the EV + CurrentLimits(entity spineapi.EntityRemoteInterface) ([]float64, []float64, []float64, error) + + // return the current loadcontrol recommendation limits + // + // parameters: + // - entity: the entity of the EV + // + // return values: + // - limits: per phase data + // + // possible errors: + // - ErrDataNotAvailable if no such limit is (yet) available + // - and others + LoadControlLimits(entity spineapi.EntityRemoteInterface) (limits []api.LoadLimitsPhase, resultErr error) + + // send new LoadControlLimits to the remote EV + // + // parameters: + // - entity: the entity of the EV + // - limits: a set of limits containing phase specific limit data + // + // recommendations: + // Sets a recommended charge power in A for each phase. This is mainly + // used if the EV and EVSE communicate via ISO15118 to support charging excess solar power. + // The EV either needs to support the Optimization of Self Consumption usecase or + // the EVSE needs to be able map the recommendations into oligation limits which then + // works for all EVs communication either via IEC61851 or ISO15118. + WriteLoadControlLimits(entity spineapi.EntityRemoteInterface, limits []api.LoadLimitsPhase) (*model.MsgCounterType, error) + + // Scenario 2 + + // this is automatically covered by the SPINE implementation + + // Scenario 3 + + // this is covered by the central CEM interface implementation + // use that one to set the CEM's operation state which will inform all remote devices +} diff --git a/ucoscev/events.go b/ucoscev/events.go new file mode 100644 index 0000000..aff48d0 --- /dev/null +++ b/ucoscev/events.go @@ -0,0 +1,62 @@ +package ucoscev + +import ( + "github.com/enbility/cemd/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// handle SPINE events +func (e *UCOSCEV) HandleEvent(payload spineapi.EventPayload) { + // most of the events are identical to OPEV, and OPEV is required to be used, + // we don't handle the same events in here + + if !util.IsCompatibleEntity(payload.Entity, e.validEntityTypes) { + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + + // the codefactor warning is invalid, as .(type) check can not be replaced with if then + //revive:disable-next-line + switch payload.Data.(type) { + case *model.ElectricalConnectionPermittedValueSetListDataType: + e.evElectricalPermittedValuesUpdate(payload) + case *model.LoadControlLimitListDataType: + e.evLoadControlLimitDataUpdate(payload) + } +} + +// the load control limit data of an EV was updated +func (e *UCOSCEV) evLoadControlLimitDataUpdate(payload spineapi.EventPayload) { + if util.LoadControlLimitsCheckPayloadDataForTypeCategoryDirectionScope(false, + e.service, payload, model.LoadControlLimitTypeTypeMaxValueLimit, + model.LoadControlCategoryTypeRecommendation, "", + model.ScopeTypeTypeSelfConsumption) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateLimit) + } +} + +// the electrical connection permitted value sets data of an EV was updated +func (e *UCOSCEV) evElectricalPermittedValuesUpdate(payload spineapi.EventPayload) { + evElectricalConnection, err := util.ElectricalConnection(e.service, payload.Entity) + if err != nil { + return + } + + data, err := evElectricalConnection.GetParameterDescriptionForMeasuredPhase(model.ElectricalConnectionPhaseNameTypeA) + if err != nil || data.ParameterId == nil { + return + } + + values, err := evElectricalConnection.GetPermittedValueSetForParameterId(*data.ParameterId) + if err != nil || values == nil { + return + } + + // Scenario 6 + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateCurrentLimits) +} diff --git a/ucoscev/events_test.go b/ucoscev/events_test.go new file mode 100644 index 0000000..582e8ca --- /dev/null +++ b/ucoscev/events_test.go @@ -0,0 +1,160 @@ +package ucoscev + +import ( + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCOSCEVSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.evEntity + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeEntityChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.Data = eebusutil.Ptr(model.ElectricalConnectionPermittedValueSetListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.LoadControlLimitListDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *UCOSCEVSuite) Test_evElectricalPermittedValuesUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evElectricalPermittedValuesUpdate(payload) + + payload.Entity = s.evEntity + s.sut.evElectricalPermittedValuesUpdate(payload) + + paramData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(1)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(2)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeC), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evElectricalPermittedValuesUpdate(payload) + + data := &model.ElectricalConnectionPermittedValueSetListDataType{ + ElectricalConnectionPermittedValueSetData: []model.ElectricalConnectionPermittedValueSetDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(0)), + PermittedValueSet: []model.ScaledNumberSetType{ + { + Value: []model.ScaledNumberType{ + *model.NewScaledNumberType(0.1), + }, + Range: []model.ScaledNumberRangeType{ + { + Min: model.NewScaledNumberType(1400), + Max: model.NewScaledNumberType(11000), + }, + }, + }, + }, + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(1)), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(2)), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeElectricalConnectionPermittedValueSetListData, data, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evElectricalPermittedValuesUpdate(payload) +} + +func (s *UCOSCEVSuite) Test_evLoadControlLimitDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evLoadControlLimitDataUpdate(payload) + + payload.Entity = s.evEntity + s.sut.evLoadControlLimitDataUpdate(payload) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + LimitCategory: eebusutil.Ptr(model.LoadControlCategoryTypeRecommendation), + LimitType: eebusutil.Ptr(model.LoadControlLimitTypeTypeMaxValueLimit), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeSelfConsumption), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeLoadControlLimitDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evLoadControlLimitDataUpdate(payload) + + data := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{}, + } + + payload.Data = data + + s.sut.evLoadControlLimitDataUpdate(payload) + + data = &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + Value: model.NewScaledNumberType(16), + }, + }, + } + + payload.Data = data + + s.sut.evLoadControlLimitDataUpdate(payload) +} diff --git a/ucoscev/public.go b/ucoscev/public.go new file mode 100644 index 0000000..6da8be8 --- /dev/null +++ b/ucoscev/public.go @@ -0,0 +1,53 @@ +package ucoscev + +import ( + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// return the min, max, default limits for each phase of the connected EV +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (e *UCOSCEV) CurrentLimits(entity spineapi.EntityRemoteInterface) ([]float64, []float64, []float64, error) { + return util.GetPhaseCurrentLimits(e.service, entity, e.validEntityTypes) +} + +// return the current loadcontrol recommendation limits +// +// parameters: +// - entity: the entity of the EV +// +// return values: +// - limits: per phase data +// +// possible errors: +// - ErrDataNotAvailable if no such limit is (yet) available +// - and others +func (e *UCOSCEV) LoadControlLimits(entity spineapi.EntityRemoteInterface) (limits []api.LoadLimitsPhase, resultErr error) { + return util.LoadControlLimits( + e.service, + entity, + e.validEntityTypes, + model.LoadControlLimitTypeTypeMaxValueLimit, + model.LoadControlCategoryTypeRecommendation, + model.ScopeTypeTypeSelfConsumption) +} + +// send new LoadControlLimits to the remote EV +// +// parameters: +// - limits: a set of limits containing phase specific limit data +// +// recommendations: +// Sets a recommended charge power in A for each phase. This is mainly +// used if the EV and EVSE communicate via ISO15118 to support charging excess solar power. +// The EV either needs to support the Optimization of Self Consumption usecase or +// the EVSE needs to be able map the recommendations into oligation limits which then +// works for all EVs communication either via IEC61851 or ISO15118. +func (e *UCOSCEV) WriteLoadControlLimits(entity spineapi.EntityRemoteInterface, limits []api.LoadLimitsPhase) (*model.MsgCounterType, error) { + return util.WriteLoadControlLimits(e.service, entity, e.validEntityTypes, model.LoadControlCategoryTypeRecommendation, limits) +} diff --git a/ucoscev/public_test.go b/ucoscev/public_test.go new file mode 100644 index 0000000..12511b4 --- /dev/null +++ b/ucoscev/public_test.go @@ -0,0 +1,28 @@ +package ucoscev + +import ( + "github.com/enbility/cemd/api" + "github.com/stretchr/testify/assert" +) + +func (s *UCOSCEVSuite) Test_Public() { + // The actual tests of the functionality is located in the util package + + _, _, _, err := s.sut.CurrentLimits(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, _, _, err = s.sut.CurrentLimits(s.evEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.LoadControlLimits(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.LoadControlLimits(s.evEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.WriteLoadControlLimits(s.mockRemoteEntity, []api.LoadLimitsPhase{}) + assert.NotNil(s.T(), err) + + _, err = s.sut.WriteLoadControlLimits(s.evEntity, []api.LoadLimitsPhase{}) + assert.NotNil(s.T(), err) +} diff --git a/ucoscev/testhelper_test.go b/ucoscev/testhelper_test.go new file mode 100644 index 0000000..06af093 --- /dev/null +++ b/ucoscev/testhelper_test.go @@ -0,0 +1,195 @@ +package ucoscev + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/cemd/api" + eebusapi "github.com/enbility/eebus-go/api" + eebusmocks "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestUCOSCEVSuite(t *testing.T) { + suite.Run(t, new(UCOSCEVSuite)) +} + +type UCOSCEVSuite struct { + suite.Suite + + sut *UCOSCEV + + service eebusapi.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *mocks.EntityRemoteInterface + evEntity spineapi.EntityRemoteInterface +} + +func (s *UCOSCEVSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { +} + +func (s *UCOSCEVSuite) BeforeTest(suiteName, testName string) { + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := eebusapi.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := eebusmocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := mocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = mocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := mocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + s.sut = NewUCOSCEV(s.service, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + var entities []spineapi.EntityRemoteInterface + + s.remoteDevice, entities = setupDevices(s.service, s.T()) + s.evEntity = entities[1] +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService eebusapi.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + []spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + role model.RoleType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeElectricalConnection, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + model.FunctionTypeElectricalConnectionPermittedValueSetListData, + }, + }, + {model.FeatureTypeTypeLoadControl, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeLoadControlLimitDescriptionListData, + model.FunctionTypeLoadControlLimitConstraintsListData, + model.FunctionTypeLoadControlLimitListData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + model.FunctionTypeElectricalConnectionPermittedValueSetListData, + }, + }, + {model.FeatureTypeTypeDeviceDiagnosis, + model.RoleTypeClient, + []model.FunctionType{}, + }, + } + + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: eebusutil.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + Feature: eebusutil.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: eebusutil.Ptr(feature.featureType), + Role: eebusutil.Ptr(feature.role), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeEVSE), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeEV), + }, + }, + }, + FeatureInformation: featureInformations, + } + + entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + remoteDevice.UpdateDevice(detailedData.DeviceInformation.Description) + + localDevice.AddRemoteDeviceForSki(remoteSki, remoteDevice) + + return remoteDevice, entities +} diff --git a/ucoscev/types.go b/ucoscev/types.go new file mode 100644 index 0000000..fe79145 --- /dev/null +++ b/ucoscev/types.go @@ -0,0 +1,17 @@ +package ucoscev + +import "github.com/enbility/cemd/api" + +const ( + // EV current limits + // + // Use `CurrentLimits` to get the current data + DataUpdateCurrentLimits api.EventType = "DataUpdateCurrentLimits" + + // EV load control recommendation limit data updated + // + // Use `LoadControlLimits` to get the current data + // + // Use Case OSCEV, Scenario 1 + DataUpdateLimit api.EventType = "DataUpdateLimit" +) diff --git a/ucoscev/ucoscev.go b/ucoscev/ucoscev.go new file mode 100644 index 0000000..1efb1c7 --- /dev/null +++ b/ucoscev/ucoscev.go @@ -0,0 +1,115 @@ +package ucoscev + +import ( + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +type UCOSCEV struct { + service eebusapi.ServiceInterface + + eventCB api.EntityEventCallback + + validEntityTypes []model.EntityTypeType +} + +var _ UCOSCEVInterface = (*UCOSCEV)(nil) + +func NewUCOSCEV(service eebusapi.ServiceInterface, eventCB api.EntityEventCallback) *UCOSCEV { + uc := &UCOSCEV{ + service: service, + eventCB: eventCB, + } + + uc.validEntityTypes = []model.EntityTypeType{ + model.EntityTypeTypeCompressor, + model.EntityTypeTypeElectricalImmersionHeater, + model.EntityTypeTypeEV, + model.EntityTypeTypeHeatPumpAppliance, + model.EntityTypeTypeInverter, + model.EntityTypeTypeSmartEnergyAppliance, + model.EntityTypeTypeSubMeterElectricity, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (c *UCOSCEV) UseCaseName() model.UseCaseNameType { + return model.UseCaseNameTypeOptimizationOfSelfConsumptionDuringEVCharging +} + +func (e *UCOSCEV) AddFeatures() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeLoadControl, + model.FeatureTypeTypeElectricalConnection, + } + for _, feature := range clientFeatures { + _ = localEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } + + // server features + f := localEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeDeviceDiagnosisStateData, true, false) + f.AddFunctionType(model.FunctionTypeDeviceDiagnosisHeartbeatData, true, false) +} + +func (e *UCOSCEV) AddUseCase() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.AddUseCaseSupport( + model.UseCaseActorTypeCEM, + e.UseCaseName(), + model.SpecificationVersionType("1.0.1"), + "release", + true, + []model.UseCaseScenarioSupportType{1, 2, 3}) +} + +func (e *UCOSCEV) UpdateUseCaseAvailability(available bool) { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.SetUseCaseAvailability(model.UseCaseActorTypeCEM, e.UseCaseName(), available) +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *UCOSCEV) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if entity == nil || entity.EntityType() != model.EntityTypeTypeEV { + return false, api.ErrNoCompatibleEntity + } + + // check if the usecase and mandatory scenarios are supported and + // if the required server features are available + if !entity.Device().VerifyUseCaseScenariosAndFeaturesSupport( + model.UseCaseActorTypeEV, + e.UseCaseName(), + []model.UseCaseScenarioSupportType{1, 2, 3}, + []model.FeatureTypeType{model.FeatureTypeTypeLoadControl}, + ) { + return false, nil + } + + // check if loadcontrol limit descriptions contains a recommendation category + evLoadControl, err := util.LoadControl(e.service, entity) + if err != nil { + return false, eebusapi.ErrFunctionNotSupported + } + + if _, err = evLoadControl.GetLimitDescriptionsForCategory(model.LoadControlCategoryTypeRecommendation); err != nil { + return false, err + } + + return true, nil +} diff --git a/ucoscev/ucoscev_test.go b/ucoscev/ucoscev_test.go new file mode 100644 index 0000000..0d26823 --- /dev/null +++ b/ucoscev/ucoscev_test.go @@ -0,0 +1,62 @@ +package ucoscev + +import ( + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCOSCEVSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *UCOSCEVSuite) Test_IsUseCaseSupported() { + data, err := s.sut.IsUseCaseSupported(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + data, err = s.sut.IsUseCaseSupported(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: eebusutil.Ptr(model.UseCaseActorTypeEV), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: eebusutil.Ptr(model.UseCaseNameTypeOptimizationOfSelfConsumptionDuringEVCharging), + UseCaseAvailable: eebusutil.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1, 2, 3}, + }, + }, + }, + }, + } + + nodemgmtEntity := s.remoteDevice.Entity([]model.AddressEntityType{0}) + nodeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(nodemgmtEntity, model.FeatureTypeTypeNodeManagement, model.RoleTypeSpecial) + fErr := nodeFeature.UpdateData(model.FunctionTypeNodeManagementUseCaseData, ucData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + LimitCategory: eebusutil.Ptr(model.LoadControlCategoryTypeRecommendation), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + fErr = rFeature.UpdateData(model.FunctionTypeLoadControlLimitDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/ucvabd/api.go b/ucvabd/api.go new file mode 100644 index 0000000..ac86dce --- /dev/null +++ b/ucvabd/api.go @@ -0,0 +1,45 @@ +package ucvabd + +import ( + "github.com/enbility/cemd/api" + spineapi "github.com/enbility/spine-go/api" +) + +//go:generate mockery + +// interface for the Visualization of Aggregated Battery Data UseCase +type UCVABDInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // return the current (dis)charging power + // + // parameters: + // - entity: the entity of the inverter + Power(entity spineapi.EntityRemoteInterface) (float64, error) + + // Scenario 2 + + // return the cumulated battery system charge energy + // + // parameters: + // - entity: the entity of the inverter + EnergyCharged(entity spineapi.EntityRemoteInterface) (float64, error) + + // Scenario 3 + + // return the cumulated battery system discharge energy + // + // parameters: + // - entity: the entity of the inverter + EnergyDischarged(entity spineapi.EntityRemoteInterface) (float64, error) + + // Scenario 4 + + // return the current state of charge of the battery system + // + // parameters: + // - entity: the entity of the inverter + StateOfCharge(entity spineapi.EntityRemoteInterface) (float64, error) +} diff --git a/ucvabd/events.go b/ucvabd/events.go new file mode 100644 index 0000000..e4aa10c --- /dev/null +++ b/ucvabd/events.go @@ -0,0 +1,100 @@ +package ucvabd + +import ( + "github.com/enbility/cemd/util" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// handle SPINE events +func (e *UCVABD) HandleEvent(payload spineapi.EventPayload) { + // only about events from an SGMW entity or device changes for this remote device + + if !util.IsCompatibleEntity(payload.Entity, e.validEntityTypes) { + return + } + + if util.IsEntityConnected(payload) { + e.inverterConnected(payload.Entity) + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + + switch payload.Data.(type) { + case *model.MeasurementDescriptionListDataType: + e.inverterMeasurementDescriptionDataUpdate(payload.Entity) + case *model.MeasurementListDataType: + e.inverterMeasurementDataUpdate(payload) + } +} + +// process required steps when a grid device is connected +func (e *UCVABD) inverterConnected(entity spineapi.EntityRemoteInterface) { + if electricalConnection, err := util.ElectricalConnection(e.service, entity); err == nil { + if _, err := electricalConnection.Subscribe(); err != nil { + logging.Log().Error(err) + } + + // get electrical connection parameter + if _, err := electricalConnection.RequestDescriptions(); err != nil { + logging.Log().Error(err) + } + + if _, err := electricalConnection.RequestParameterDescriptions(); err != nil { + logging.Log().Error(err) + } + } + + if measurement, err := util.Measurement(e.service, entity); err == nil { + if _, err := measurement.Subscribe(); err != nil { + logging.Log().Error(err) + } + + // get measurement parameters + if _, err := measurement.RequestDescriptions(); err != nil { + logging.Log().Error(err) + } + + if _, err := measurement.RequestConstraints(); err != nil { + logging.Log().Error(err) + } + } +} + +// the measurement descriptiondata of an SMGW was updated +func (e *UCVABD) inverterMeasurementDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if measurement, err := util.Measurement(e.service, entity); err == nil { + // measurement descriptions received, now get the data + if _, err := measurement.RequestValues(); err != nil { + logging.Log().Error("Error getting measurement list values:", err) + } + } +} + +// the measurement data of an SMGW was updated +func (e *UCVABD) inverterMeasurementDataUpdate(payload spineapi.EventPayload) { + // Scenario 1 + if util.MeasurementCheckPayloadDataForScope(e.service, payload, model.ScopeTypeTypeACPowerTotal) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePower) + } + + // Scenario 2 + if util.MeasurementCheckPayloadDataForScope(e.service, payload, model.ScopeTypeTypeCharge) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateEnergyCharged) + } + + // Scenario 3 + if util.MeasurementCheckPayloadDataForScope(e.service, payload, model.ScopeTypeTypeDischarge) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateEnergyDischarged) + } + + // Scenario 4 + if util.MeasurementCheckPayloadDataForScope(e.service, payload, model.ScopeTypeTypeStateOfCharge) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateStateOfCharge) + } +} diff --git a/ucvabd/events_test.go b/ucvabd/events_test.go new file mode 100644 index 0000000..aed4b5f --- /dev/null +++ b/ucvabd/events_test.go @@ -0,0 +1,107 @@ +package ucvabd + +import ( + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCVABDSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.batteryEntity + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeEntityChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.Data = eebusutil.Ptr(model.DeviceConfigurationKeyValueDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.DeviceConfigurationKeyValueListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.MeasurementDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.MeasurementListDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *UCVABDSuite) Test_Failures() { + s.sut.inverterConnected(s.mockRemoteEntity) + + s.sut.inverterMeasurementDescriptionDataUpdate(s.mockRemoteEntity) +} + +func (s *UCVABDSuite) Test_inverterMeasurementDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.batteryEntity, + } + s.sut.inverterMeasurementDataUpdate(payload) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeCharge), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeDischarge), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(3)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeStateOfCharge), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.batteryEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.inverterMeasurementDataUpdate(payload) + + data := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(3)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + payload.Data = data + + s.sut.inverterMeasurementDataUpdate(payload) +} diff --git a/inverterbatteryvis/public.go b/ucvabd/public.go similarity index 51% rename from inverterbatteryvis/public.go rename to ucvabd/public.go index 339b099..48672f8 100644 --- a/inverterbatteryvis/public.go +++ b/ucvabd/public.go @@ -1,9 +1,11 @@ -package inverterbatteryvis +package ucvabd import ( + "github.com/enbility/cemd/api" "github.com/enbility/cemd/util" - "github.com/enbility/eebus-go/features" - "github.com/enbility/eebus-go/spine/model" + eebusapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" ) // return the current battery (dis-)charge power (W) @@ -14,12 +16,16 @@ import ( // possible errors: // - ErrDataNotAvailable if no such measurement is (yet) available // - and others -func (i *InverterBatteryVisImpl) CurrentDisChargePower() (float64, error) { +func (e *UCVABD) Power(entity spineapi.EntityRemoteInterface) (float64, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + measurement := model.MeasurementTypeTypePower commodity := model.CommodityTypeTypeElectricity scope := model.ScopeTypeTypeACPowerTotal - data, err := i.getValuesForTypeCommodityScope(measurement, commodity, scope) + data, err := e.getValuesForTypeCommodityScope(entity, measurement, commodity, scope) if err != nil { return 0, err } @@ -28,7 +34,7 @@ func (i *InverterBatteryVisImpl) CurrentDisChargePower() (float64, error) { mId := data[0].MeasurementId value := data[0].Value if mId == nil || value == nil { - return 0, features.ErrDataNotAvailable + return 0, eebusapi.ErrDataNotAvailable } return value.GetValue(), nil @@ -39,11 +45,15 @@ func (i *InverterBatteryVisImpl) CurrentDisChargePower() (float64, error) { // possible errors: // - ErrDataNotAvailable if no such measurement is (yet) available // - and others -func (i *InverterBatteryVisImpl) TotalChargeEnergy() (float64, error) { +func (e *UCVABD) EnergyCharged(entity spineapi.EntityRemoteInterface) (float64, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + measurement := model.MeasurementTypeTypeEnergy commodity := model.CommodityTypeTypeElectricity scope := model.ScopeTypeTypeCharge - data, err := i.getValuesForTypeCommodityScope(measurement, commodity, scope) + data, err := e.getValuesForTypeCommodityScope(entity, measurement, commodity, scope) if err != nil { return 0, err } @@ -51,7 +61,7 @@ func (i *InverterBatteryVisImpl) TotalChargeEnergy() (float64, error) { // we assume thre is only one result value := data[0].Value if value == nil { - return 0, features.ErrDataNotAvailable + return 0, eebusapi.ErrDataNotAvailable } return value.GetValue(), nil @@ -62,11 +72,15 @@ func (i *InverterBatteryVisImpl) TotalChargeEnergy() (float64, error) { // possible errors: // - ErrDataNotAvailable if no such measurement is (yet) available // - and others -func (i *InverterBatteryVisImpl) TotalDischargeEnergy() (float64, error) { +func (e *UCVABD) EnergyDischarged(entity spineapi.EntityRemoteInterface) (float64, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + measurement := model.MeasurementTypeTypeEnergy commodity := model.CommodityTypeTypeElectricity scope := model.ScopeTypeTypeDischarge - data, err := i.getValuesForTypeCommodityScope(measurement, commodity, scope) + data, err := e.getValuesForTypeCommodityScope(entity, measurement, commodity, scope) if err != nil { return 0, err } @@ -74,7 +88,7 @@ func (i *InverterBatteryVisImpl) TotalDischargeEnergy() (float64, error) { // we assume thre is only one result value := data[0].Value if value == nil { - return 0, features.ErrDataNotAvailable + return 0, eebusapi.ErrDataNotAvailable } return value.GetValue(), nil @@ -85,11 +99,15 @@ func (i *InverterBatteryVisImpl) TotalDischargeEnergy() (float64, error) { // possible errors: // - ErrDataNotAvailable if no such measurement is (yet) available // - and others -func (i *InverterBatteryVisImpl) CurrentStateOfCharge() (float64, error) { +func (e *UCVABD) StateOfCharge(entity spineapi.EntityRemoteInterface) (float64, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + measurement := model.MeasurementTypeTypePercentage commodity := model.CommodityTypeTypeElectricity scope := model.ScopeTypeTypeStateOfCharge - data, err := i.getValuesForTypeCommodityScope(measurement, commodity, scope) + data, err := e.getValuesForTypeCommodityScope(entity, measurement, commodity, scope) if err != nil { return 0, err } @@ -97,7 +115,7 @@ func (i *InverterBatteryVisImpl) CurrentStateOfCharge() (float64, error) { // we assume thre is only one result value := data[0].Value if value == nil { - return 0, features.ErrDataNotAvailable + return 0, eebusapi.ErrDataNotAvailable } return value.GetValue(), nil @@ -105,14 +123,19 @@ func (i *InverterBatteryVisImpl) CurrentStateOfCharge() (float64, error) { // helper -func (i *InverterBatteryVisImpl) getValuesForTypeCommodityScope(measurement model.MeasurementTypeType, commodity model.CommodityTypeType, scope model.ScopeTypeType) ([]model.MeasurementDataType, error) { - if i.inverterEntity == nil { +func (e *UCVABD) getValuesForTypeCommodityScope( + entity spineapi.EntityRemoteInterface, + measurement model.MeasurementTypeType, + commodity model.CommodityTypeType, + scope model.ScopeTypeType) ([]model.MeasurementDataType, error) { + if entity == nil { return nil, util.ErrDeviceDisconnected } - if i.inverterMeasurement == nil { - return nil, features.ErrDataNotAvailable + measurementF, err := util.Measurement(e.service, entity) + if err != nil { + return nil, eebusapi.ErrFunctionNotSupported } - return i.inverterMeasurement.GetValuesForTypeCommodityScope(measurement, commodity, scope) + return measurementF.GetValuesForTypeCommodityScope(measurement, commodity, scope) } diff --git a/ucvabd/public_test.go b/ucvabd/public_test.go new file mode 100644 index 0000000..5dab7eb --- /dev/null +++ b/ucvabd/public_test.go @@ -0,0 +1,187 @@ +package ucvabd + +import ( + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCVABDSuite) Test_CurrentChargePower() { + data, err := s.sut.Power(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.Power(s.batteryEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypePower), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + }, + } + + measurementFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.batteryEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := measurementFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Power(s.batteryEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = measurementFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Power(s.batteryEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} + +func (s *UCVABDSuite) Test_TotalChargeEnergy() { + data, err := s.sut.EnergyCharged(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.EnergyCharged(s.batteryEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeCharge), + }, + }, + } + + measurementFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.batteryEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := measurementFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyCharged(s.batteryEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = measurementFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyCharged(s.batteryEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} + +func (s *UCVABDSuite) Test_TotalDischargeEnergy() { + data, err := s.sut.EnergyDischarged(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.EnergyDischarged(s.batteryEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeDischarge), + }, + }, + } + + measurementFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.batteryEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := measurementFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyDischarged(s.batteryEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = measurementFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyDischarged(s.batteryEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} + +func (s *UCVABDSuite) Test_CurrentStateOfCharge() { + data, err := s.sut.StateOfCharge(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.StateOfCharge(s.batteryEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypePercentage), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeStateOfCharge), + }, + }, + } + + measurementFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.batteryEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := measurementFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.StateOfCharge(s.batteryEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = measurementFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.StateOfCharge(s.batteryEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} diff --git a/ucvabd/testhelper_test.go b/ucvabd/testhelper_test.go new file mode 100644 index 0000000..a8fd708 --- /dev/null +++ b/ucvabd/testhelper_test.go @@ -0,0 +1,169 @@ +package ucvabd + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/cemd/api" + eebusapi "github.com/enbility/eebus-go/api" + eebusmocks "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestVABDSuite(t *testing.T) { + suite.Run(t, new(UCVABDSuite)) +} + +type UCVABDSuite struct { + suite.Suite + + sut *UCVABD + + service eebusapi.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *mocks.EntityRemoteInterface + batteryEntity spineapi.EntityRemoteInterface +} + +func (s *UCVABDSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { +} + +func (s *UCVABDSuite) BeforeTest(suiteName, testName string) { + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := eebusapi.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := eebusmocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := mocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = mocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := mocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + s.sut = NewUCVABD(s.service, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + s.remoteDevice, s.batteryEntity = setupDevices(s.service, s.T()) +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService eebusapi.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeMeasurement, + []model.FunctionType{ + model.FunctionTypeMeasurementDescriptionListData, + model.FunctionTypeMeasurementConstraintsListData, + model.FunctionTypeMeasurementListData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + model.FunctionTypeElectricalConnectionDescriptionListData, + }, + }, + } + + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: eebusutil.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + Feature: eebusutil.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: eebusutil.Ptr(feature.featureType), + Role: eebusutil.Ptr(model.RoleTypeServer), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeElectricityStorageSystem), + }, + }, + }, + FeatureInformation: featureInformations, + } + + entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + remoteDevice.UpdateDevice(detailedData.DeviceInformation.Description) + + localDevice.AddRemoteDeviceForSki(remoteSki, remoteDevice) + + return remoteDevice, entities[0] +} diff --git a/ucvabd/types.go b/ucvabd/types.go new file mode 100644 index 0000000..c93ddce --- /dev/null +++ b/ucvabd/types.go @@ -0,0 +1,33 @@ +package ucvabd + +import "github.com/enbility/cemd/api" + +const ( + // Battery System (dis)charge power data updated + // + // Use `Power` to get the current data + // + // Use Case VABD, Scenario 1 + DataUpdatePower api.EventType = "d" + + // Battery System cumulated charge energy data updated + // + // Use `EnergyCharged` to get the current data + // + // Use Case VABD, Scenario 2 + DataUpdateEnergyCharged api.EventType = "DataUpdateEnergyCharged" + + // Battery System cumulated discharge energy data updated + // + // Use `EnergyDischarged` to get the current data + // + // Use Case VABD, Scenario 2 + DataUpdateEnergyDischarged api.EventType = "DataUpdateEnergyDischarged" + + // Battery System state of charge data updated + // + // Use `StateOfCharge` to get the current data + // + // Use Case VABD, Scenario 4 + DataUpdateStateOfCharge api.EventType = "DataUpdateStateOfCharge" +) diff --git a/ucvabd/ucvabd.go b/ucvabd/ucvabd.go new file mode 100644 index 0000000..7b824f2 --- /dev/null +++ b/ucvabd/ucvabd.go @@ -0,0 +1,126 @@ +package ucvabd + +import ( + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +type UCVABD struct { + service eebusapi.ServiceInterface + + eventCB api.EntityEventCallback + + validEntityTypes []model.EntityTypeType +} + +var _ UCVABDInterface = (*UCVABD)(nil) + +func NewUCVABD(service eebusapi.ServiceInterface, eventCB api.EntityEventCallback) *UCVABD { + uc := &UCVABD{ + service: service, + eventCB: eventCB, + } + + uc.validEntityTypes = []model.EntityTypeType{ + model.EntityTypeTypeElectricityStorageSystem, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (c *UCVABD) UseCaseName() model.UseCaseNameType { + return model.UseCaseNameTypeVisualizationOfAggregatedBatteryData +} + +func (e *UCVABD) AddFeatures() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceConfiguration, + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + } + for _, feature := range clientFeatures { + _ = localEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } +} + +func (e *UCVABD) AddUseCase() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.AddUseCaseSupport( + model.UseCaseActorTypeCEM, + e.UseCaseName(), + model.SpecificationVersionType("1.0.1"), + "RC1", + true, + []model.UseCaseScenarioSupportType{1, 2, 3}) +} + +func (e *UCVABD) UpdateUseCaseAvailability(available bool) { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.SetUseCaseAvailability(model.UseCaseActorTypeCEM, e.UseCaseName(), available) +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *UCVABD) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return false, api.ErrNoCompatibleEntity + } + + // check if the usecase and mandatory scenarios are supported and + // if the required server features are available + if !entity.Device().VerifyUseCaseScenariosAndFeaturesSupport( + model.UseCaseActorTypePVSystem, + e.UseCaseName(), + []model.UseCaseScenarioSupportType{1, 4}, + []model.FeatureTypeType{ + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + }, + ) { + return false, nil + } + + // check for required features + electricalConnection, err := util.ElectricalConnection(e.service, entity) + if err != nil { + return false, eebusapi.ErrFunctionNotSupported + } + + // check if electrical connection descriptions and parameter descriptions are available name + if _, err = electricalConnection.GetDescriptions(); err != nil { + return false, err + } + if _, err = electricalConnection.GetParameterDescriptions(); err != nil { + return false, err + } + + // check for required features + measurement, err := util.Measurement(e.service, entity) + if err != nil { + return false, eebusapi.ErrFunctionNotSupported + } + + // check if measurement descriptions contains a required scope + if _, err = measurement.GetDescriptionsForScope(model.ScopeTypeTypeACPowerTotal); err != nil { + return false, err + } + if _, err = measurement.GetDescriptionsForScope(model.ScopeTypeTypeStateOfCharge); err != nil { + return false, err + } + + return true, nil +} diff --git a/ucvabd/ucvabd_test.go b/ucvabd/ucvabd_test.go new file mode 100644 index 0000000..e73e3c5 --- /dev/null +++ b/ucvabd/ucvabd_test.go @@ -0,0 +1,97 @@ +package ucvabd + +import ( + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCVABDSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *UCVABDSuite) Test_IsUseCaseSupported() { + data, err := s.sut.IsUseCaseSupported(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + data, err = s.sut.IsUseCaseSupported(s.batteryEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: eebusutil.Ptr(model.UseCaseActorTypePVSystem), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: eebusutil.Ptr(model.UseCaseNameTypeVisualizationOfAggregatedBatteryData), + UseCaseAvailable: eebusutil.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1, 4}, + }, + }, + }, + }, + } + + nodemgmtEntity := s.remoteDevice.Entity([]model.AddressEntityType{0}) + nodeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(nodemgmtEntity, model.FeatureTypeTypeNodeManagement, model.RoleTypeSpecial) + fErr := nodeFeature.UpdateData(model.FunctionTypeNodeManagementUseCaseData, ucData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.batteryEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + elData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + }, + }, + } + + elFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.batteryEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = elFeature.UpdateData(model.FunctionTypeElectricalConnectionDescriptionListData, elData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.batteryEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + paramData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + }, + }, + } + + fErr = elFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.batteryEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeStateOfCharge), + }, + }, + } + + measurementFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.batteryEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr = measurementFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.batteryEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/ucvapd/api.go b/ucvapd/api.go new file mode 100644 index 0000000..293a84a --- /dev/null +++ b/ucvapd/api.go @@ -0,0 +1,37 @@ +package ucvapd + +import ( + "github.com/enbility/cemd/api" + spineapi "github.com/enbility/spine-go/api" +) + +//go:generate mockery + +// interface for the Visualization of Aggregated Photovoltaic Data UseCase +type UCVAPDInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // return the current production power + // + // parameters: + // - entity: the entity of the inverter + Power(entity spineapi.EntityRemoteInterface) (float64, error) + + // Scenario 2 + + // return the nominal peak power + // + // parameters: + // - entity: the entity of the inverter + PowerNominalPeak(entity spineapi.EntityRemoteInterface) (float64, error) + + // Scenario 3 + + // return total PV yield + // + // parameters: + // - entity: the entity of the inverter + PVYieldTotal(entity spineapi.EntityRemoteInterface) (float64, error) +} diff --git a/ucvapd/events.go b/ucvapd/events.go new file mode 100644 index 0000000..3210dfb --- /dev/null +++ b/ucvapd/events.go @@ -0,0 +1,127 @@ +package ucvapd + +import ( + "github.com/enbility/cemd/util" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// handle SPINE events +func (e *UCVAPD) HandleEvent(payload spineapi.EventPayload) { + // only about events from an SGMW entity or device changes for this remote device + + if !util.IsCompatibleEntity(payload.Entity, e.validEntityTypes) { + return + } + + if util.IsEntityConnected(payload) { + e.inverterConnected(payload.Entity) + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + + switch payload.Data.(type) { + case *model.DeviceConfigurationKeyValueDescriptionListDataType: + e.inverterConfigurationDescriptionDataUpdate(payload.Entity) + case *model.DeviceConfigurationKeyValueListDataType: + e.inverterConfigurationDataUpdate(payload) + case *model.MeasurementDescriptionListDataType: + e.inverterMeasurementDescriptionDataUpdate(payload.Entity) + case *model.MeasurementListDataType: + e.inverterMeasurementDataUpdate(payload) + } +} + +// process required steps when a grid device is connected +func (e *UCVAPD) inverterConnected(entity spineapi.EntityRemoteInterface) { + if deviceConfiguration, err := util.DeviceConfiguration(e.service, entity); err == nil { + if _, err := deviceConfiguration.Subscribe(); err != nil { + logging.Log().Error(err) + } + + // get configuration data + if _, err := deviceConfiguration.RequestDescriptions(); err != nil { + logging.Log().Error(err) + } + } + + if electricalConnection, err := util.ElectricalConnection(e.service, entity); err == nil { + if _, err := electricalConnection.Subscribe(); err != nil { + logging.Log().Error(err) + } + + // get electrical connection parameter + if _, err := electricalConnection.RequestDescriptions(); err != nil { + logging.Log().Error(err) + } + + if _, err := electricalConnection.RequestParameterDescriptions(); err != nil { + logging.Log().Error(err) + } + } + + if measurement, err := util.Measurement(e.service, entity); err == nil { + if _, err := measurement.Subscribe(); err != nil { + logging.Log().Error(err) + } + + // get measurement parameters + if _, err := measurement.RequestDescriptions(); err != nil { + logging.Log().Error(err) + } + + if _, err := measurement.RequestConstraints(); err != nil { + logging.Log().Error(err) + } + } +} + +// the configuration key description data of an SMGW was updated +func (e *UCVAPD) inverterConfigurationDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if deviceConfiguration, err := util.DeviceConfiguration(e.service, entity); err == nil { + // key value descriptions received, now get the data + if _, err := deviceConfiguration.RequestKeyValues(); err != nil { + logging.Log().Error("Error getting configuration key values:", err) + } + } +} + +// the measurement data of an SMGW was updated +func (e *UCVAPD) inverterConfigurationDataUpdate(payload spineapi.EventPayload) { + // Scenario 1 + if deviceConfiguration, err := util.DeviceConfiguration(e.service, payload.Entity); err == nil { + if _, err := deviceConfiguration.GetKeyValueForKeyName( + model.DeviceConfigurationKeyNameTypePeakPowerOfPVSystem, + model.DeviceConfigurationKeyValueTypeTypeScaledNumber); err == nil { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePowerNominalPeak) + } + } +} + +// the measurement descriptiondata of an SMGW was updated +func (e *UCVAPD) inverterMeasurementDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if measurement, err := util.Measurement(e.service, entity); err == nil { + // measurement descriptions received, now get the data + if _, err := measurement.RequestValues(); err != nil { + logging.Log().Error("Error getting measurement list values:", err) + } + } +} + +// the measurement data of an SMGW was updated +func (e *UCVAPD) inverterMeasurementDataUpdate(payload spineapi.EventPayload) { + // Scenario 2 + if util.MeasurementCheckPayloadDataForScope(e.service, payload, model.ScopeTypeTypeACPowerTotal) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePower) + } + + // Scenario 3 + if util.MeasurementCheckPayloadDataForScope(e.service, payload, model.ScopeTypeTypeACYieldTotal) { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePVYieldTotal) + } +} diff --git a/ucvapd/events_test.go b/ucvapd/events_test.go new file mode 100644 index 0000000..493bc94 --- /dev/null +++ b/ucvapd/events_test.go @@ -0,0 +1,136 @@ +package ucvapd + +import ( + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCVAPDSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.pvEntity + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeEntityChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.Data = eebusutil.Ptr(model.DeviceConfigurationKeyValueDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.DeviceConfigurationKeyValueListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.MeasurementDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.MeasurementListDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *UCVAPDSuite) Test_Failures() { + s.sut.inverterConnected(s.mockRemoteEntity) + + s.sut.inverterConfigurationDescriptionDataUpdate(s.mockRemoteEntity) + + s.sut.inverterMeasurementDescriptionDataUpdate(s.mockRemoteEntity) +} + +func (s *UCVAPDSuite) Test_inverterConfigurationDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.pvEntity, + } + s.sut.inverterConfigurationDataUpdate(payload) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypePeakPowerOfPVSystem), + ValueType: eebusutil.Ptr(model.DeviceConfigurationKeyValueTypeTypeScaledNumber), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.pvEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.inverterConfigurationDataUpdate(payload) + + data := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(10), + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, data, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.inverterConfigurationDataUpdate(payload) +} + +func (s *UCVAPDSuite) Test_inverterMeasurementDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.pvEntity, + } + s.sut.inverterMeasurementDataUpdate(payload) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACYieldTotal), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.pvEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.inverterMeasurementDescriptionDataUpdate(payload.Entity) + + s.sut.inverterMeasurementDataUpdate(payload) + + data := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + payload.Data = data + + s.sut.inverterMeasurementDataUpdate(payload) +} diff --git a/ucvapd/public.go b/ucvapd/public.go new file mode 100644 index 0000000..0ae8d38 --- /dev/null +++ b/ucvapd/public.go @@ -0,0 +1,121 @@ +package ucvapd + +import ( + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// return the current photovoltaic production power (W) +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (e *UCVAPD) Power(entity spineapi.EntityRemoteInterface) (float64, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + + measurement := model.MeasurementTypeTypePower + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeACPowerTotal + + data, err := e.getValuesForTypeCommodityScope(entity, 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, eebusapi.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 (e *UCVAPD) PowerNominalPeak(entity spineapi.EntityRemoteInterface) (float64, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + + deviceConfiguration, err := util.DeviceConfiguration(e.service, entity) + if err != nil { + return 0, eebusapi.ErrFunctionNotSupported + } + + keyName := model.DeviceConfigurationKeyNameTypePeakPowerOfPVSystem + if _, err = deviceConfiguration.GetDescriptionForKeyName(keyName); err != nil { + return 0, err + } + + data, err := deviceConfiguration.GetKeyValueForKeyName(keyName, model.DeviceConfigurationKeyValueTypeTypeScaledNumber) + if err != nil { + return 0, err + } + + if data == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + value, ok := data.(*model.ScaledNumberType) + if !ok || value == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + return value.GetValue(), nil +} + +// return the total photovoltaic yield (Wh) +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (e *UCVAPD) PVYieldTotal(entity spineapi.EntityRemoteInterface) (float64, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + + measurement := model.MeasurementTypeTypeEnergy + commodity := model.CommodityTypeTypeElectricity + scope := model.ScopeTypeTypeACYieldTotal + data, err := e.getValuesForTypeCommodityScope(entity, 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, eebusapi.ErrDataNotAvailable + } + + return value.GetValue(), nil +} + +// helper + +func (e *UCVAPD) getValuesForTypeCommodityScope( + entity spineapi.EntityRemoteInterface, + measurement model.MeasurementTypeType, + commodity model.CommodityTypeType, + scope model.ScopeTypeType) ([]model.MeasurementDataType, error) { + if entity == nil { + return nil, util.ErrDeviceDisconnected + } + + measurementF, err := util.Measurement(e.service, entity) + if err != nil { + return nil, eebusapi.ErrFunctionNotSupported + } + + return measurementF.GetValuesForTypeCommodityScope(measurement, commodity, scope) +} diff --git a/ucvapd/public_test.go b/ucvapd/public_test.go new file mode 100644 index 0000000..ace8ccc --- /dev/null +++ b/ucvapd/public_test.go @@ -0,0 +1,137 @@ +package ucvapd + +import ( + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCVAPDSuite) Test_CurrentProductionPower() { + data, err := s.sut.Power(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.Power(s.pvEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypePower), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + }, + } + + measurementFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.pvEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := measurementFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Power(s.pvEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = measurementFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Power(s.pvEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} + +func (s *UCVAPDSuite) Test_NominalPeakPower() { + data, err := s.sut.PowerNominalPeak(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.PowerNominalPeak(s.pvEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + confData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypePeakPowerOfPVSystem), + }, + }, + } + + confFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.pvEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := confFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, confData, nil, nil) + assert.Nil(s.T(), fErr) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(10), + }, + }, + }, + } + fErr = confFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerNominalPeak(s.pvEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} + +func (s *UCVAPDSuite) Test_TotalPVYield() { + data, err := s.sut.PVYieldTotal(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.PVYieldTotal(s.pvEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACYieldTotal), + }, + }, + } + + measurementFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.pvEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := measurementFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PVYieldTotal(s.pvEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = measurementFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PVYieldTotal(s.pvEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} diff --git a/ucvapd/testhelper_test.go b/ucvapd/testhelper_test.go new file mode 100644 index 0000000..f1087d7 --- /dev/null +++ b/ucvapd/testhelper_test.go @@ -0,0 +1,175 @@ +package ucvapd + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/cemd/api" + eebusapi "github.com/enbility/eebus-go/api" + eebusmocks "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestVAPDSuite(t *testing.T) { + suite.Run(t, new(UCVAPDSuite)) +} + +type UCVAPDSuite struct { + suite.Suite + + sut *UCVAPD + + service eebusapi.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *mocks.EntityRemoteInterface + pvEntity spineapi.EntityRemoteInterface +} + +func (s *UCVAPDSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { +} + +func (s *UCVAPDSuite) BeforeTest(suiteName, testName string) { + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := eebusapi.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := eebusmocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := mocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = mocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := mocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + s.sut = NewUCVAPD(s.service, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + s.remoteDevice, s.pvEntity = setupDevices(s.service, s.T()) +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService eebusapi.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeMeasurement, + []model.FunctionType{ + model.FunctionTypeMeasurementDescriptionListData, + model.FunctionTypeMeasurementConstraintsListData, + model.FunctionTypeMeasurementListData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + model.FunctionTypeElectricalConnectionDescriptionListData, + }, + }, + {model.FeatureTypeTypeDeviceConfiguration, + []model.FunctionType{ + model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, + model.FunctionTypeDeviceConfigurationKeyValueListData, + }, + }, + } + + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: eebusutil.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + Feature: eebusutil.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: eebusutil.Ptr(feature.featureType), + Role: eebusutil.Ptr(model.RoleTypeServer), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypePVSystem), + }, + }, + }, + FeatureInformation: featureInformations, + } + + entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + remoteDevice.UpdateDevice(detailedData.DeviceInformation.Description) + + localDevice.AddRemoteDeviceForSki(remoteSki, remoteDevice) + + return remoteDevice, entities[0] +} diff --git a/ucvapd/types.go b/ucvapd/types.go new file mode 100644 index 0000000..52b01c0 --- /dev/null +++ b/ucvapd/types.go @@ -0,0 +1,26 @@ +package ucvapd + +import "github.com/enbility/cemd/api" + +const ( + // PV System total power data updated + // + // Use `Power` to get the current data + // + // Use Case VAPD, Scenario 1 + DataUpdatePower api.EventType = "DataUpdatePower" + + // PV System nominal peak power data updated + // + // Use `PowerNominalPeak` to get the current data + // + // Use Case VAPD, Scenario 2 + DataUpdatePowerNominalPeak api.EventType = "DataUpdatePowerNominalPeak" + + // PV System total yield data updated + // + // Use `PVYieldTotal` to get the current data + // + // Use Case VAPD, Scenario 3 + DataUpdatePVYieldTotal api.EventType = "DataUpdatePVYieldTotal" +) diff --git a/ucvapd/ucvapd.go b/ucvapd/ucvapd.go new file mode 100644 index 0000000..ea768d7 --- /dev/null +++ b/ucvapd/ucvapd.go @@ -0,0 +1,136 @@ +package ucvapd + +import ( + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +type UCVAPD struct { + service eebusapi.ServiceInterface + + eventCB api.EntityEventCallback + + validEntityTypes []model.EntityTypeType +} + +var _ UCVAPDInterface = (*UCVAPD)(nil) + +func NewUCVAPD(service eebusapi.ServiceInterface, eventCB api.EntityEventCallback) *UCVAPD { + uc := &UCVAPD{ + service: service, + eventCB: eventCB, + } + + uc.validEntityTypes = []model.EntityTypeType{ + model.EntityTypeTypePVSystem, + } + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (c *UCVAPD) UseCaseName() model.UseCaseNameType { + return model.UseCaseNameTypeVisualizationOfAggregatedPhotovoltaicData +} + +func (e *UCVAPD) AddFeatures() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceConfiguration, + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + } + for _, feature := range clientFeatures { + _ = localEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } +} + +func (e *UCVAPD) AddUseCase() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.AddUseCaseSupport( + model.UseCaseActorTypeCEM, + e.UseCaseName(), + model.SpecificationVersionType("1.0.1"), + "RC1", + true, + []model.UseCaseScenarioSupportType{1, 2, 3}) +} + +func (e *UCVAPD) UpdateUseCaseAvailability(available bool) { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.SetUseCaseAvailability(model.UseCaseActorTypeCEM, e.UseCaseName(), available) +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *UCVAPD) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return false, api.ErrNoCompatibleEntity + } + + // check if the usecase and mandatory scenarios are supported and + // if the required server features are available + if !entity.Device().VerifyUseCaseScenariosAndFeaturesSupport( + model.UseCaseActorTypePVSystem, + e.UseCaseName(), + []model.UseCaseScenarioSupportType{1, 2, 3}, + []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceConfiguration, + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + }, + ) { + return false, nil + } + + // check for required features + deviceConfiguration, err := util.DeviceConfiguration(e.service, entity) + if err != nil { + return false, eebusapi.ErrFunctionNotSupported + } + + // check if device configuration descriptions contains a required key name + if _, err = deviceConfiguration.GetDescriptionForKeyName(model.DeviceConfigurationKeyNameTypePeakPowerOfPVSystem); err != nil { + return false, err + } + + electricalConnection, err := util.ElectricalConnection(e.service, entity) + if err != nil { + return false, eebusapi.ErrFunctionNotSupported + } + + // check if electrical connection descriptions and parameter descriptions are available name + if _, err = electricalConnection.GetDescriptions(); err != nil { + return false, err + } + if _, err = electricalConnection.GetParameterDescriptions(); err != nil { + return false, err + } + + // check for required features + measurement, err := util.Measurement(e.service, entity) + if err != nil { + return false, eebusapi.ErrFunctionNotSupported + } + + // check if measurement descriptions contains a required scope + if _, err = measurement.GetDescriptionsForScope(model.ScopeTypeTypeACPowerTotal); err != nil { + return false, err + } + if _, err = measurement.GetDescriptionsForScope(model.ScopeTypeTypeACYieldTotal); err != nil { + return false, err + } + + return true, nil +} diff --git a/ucvapd/ucvapd_test.go b/ucvapd/ucvapd_test.go new file mode 100644 index 0000000..ed8e709 --- /dev/null +++ b/ucvapd/ucvapd_test.go @@ -0,0 +1,114 @@ +package ucvapd + +import ( + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCVAPDSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *UCVAPDSuite) Test_IsUseCaseSupported() { + data, err := s.sut.IsUseCaseSupported(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + data, err = s.sut.IsUseCaseSupported(s.pvEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: eebusutil.Ptr(model.UseCaseActorTypePVSystem), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: eebusutil.Ptr(model.UseCaseNameTypeVisualizationOfAggregatedPhotovoltaicData), + UseCaseAvailable: eebusutil.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1, 2, 3}, + }, + }, + }, + }, + } + + nodemgmtEntity := s.remoteDevice.Entity([]model.AddressEntityType{0}) + nodeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(nodemgmtEntity, model.FeatureTypeTypeNodeManagement, model.RoleTypeSpecial) + fErr := nodeFeature.UpdateData(model.FunctionTypeNodeManagementUseCaseData, ucData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.pvEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + confData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypePeakPowerOfPVSystem), + }, + }, + } + + confFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.pvEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr = confFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, confData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.pvEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + elData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + }, + }, + } + + elFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.pvEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = elFeature.UpdateData(model.FunctionTypeElectricalConnectionDescriptionListData, elData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.pvEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + paramData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + }, + }, + } + + fErr = elFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.pvEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACYieldTotal), + }, + }, + } + + measurementFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.pvEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr = measurementFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.pvEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/util/deviceconfiguration.go b/util/deviceconfiguration.go new file mode 100644 index 0000000..928231c --- /dev/null +++ b/util/deviceconfiguration.go @@ -0,0 +1,159 @@ +package util + +import ( + eebusapi "github.com/enbility/eebus-go/api" + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +func DeviceConfigurationCheckDataPayloadForKeyName(localServer bool, service eebusapi.ServiceInterface, + payload spineapi.EventPayload, keyName model.DeviceConfigurationKeyNameType) bool { + var desc *model.DeviceConfigurationKeyValueDescriptionDataType + var data *model.DeviceConfigurationKeyValueListDataType + + if payload.Data == nil { + return false + } + data = payload.Data.(*model.DeviceConfigurationKeyValueListDataType) + + if localServer { + desc = GetLocalDeviceConfigurationDescriptionForKeyName(service, keyName) + } else { + deviceconfigF, err := DeviceConfiguration(service, payload.Entity) + if err != nil { + return false + } + + desc, err = deviceconfigF.GetDescriptionForKeyName(keyName) + if err != nil { + return false + } + } + + for _, item := range data.DeviceConfigurationKeyValueData { + if item.KeyId == nil || *item.KeyId != *desc.KeyId || + item.Value == nil { + continue + } + + return true + } + + return false +} + +func GetLocalDeviceConfigurationDescriptionForKeyName( + service eebusapi.ServiceInterface, + keyName model.DeviceConfigurationKeyNameType, +) (description *model.DeviceConfigurationKeyValueDescriptionDataType) { + description = &model.DeviceConfigurationKeyValueDescriptionDataType{} + + localEntity := service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + deviceConfiguration := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + if deviceConfiguration == nil { + return + } + + data, err := spine.LocalFeatureDataCopyOfType[*model.DeviceConfigurationKeyValueDescriptionListDataType]( + deviceConfiguration, model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData) + if err != nil || data == nil || data.DeviceConfigurationKeyValueDescriptionData == nil { + return + } + + for _, desc := range data.DeviceConfigurationKeyValueDescriptionData { + if desc.KeyName != nil && *desc.KeyName == keyName { + return &desc + } + } + + return +} + +func GetLocalDeviceConfigurationKeyValueForKeyName( + service eebusapi.ServiceInterface, + keyName model.DeviceConfigurationKeyNameType, +) (keyData model.DeviceConfigurationKeyValueDataType) { + keyData = model.DeviceConfigurationKeyValueDataType{} + + description := GetLocalDeviceConfigurationDescriptionForKeyName(service, keyName) + if description.KeyId == nil { + return + } + + localEntity := service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + deviceConfiguration := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + if deviceConfiguration == nil { + return + } + + data, err := spine.LocalFeatureDataCopyOfType[*model.DeviceConfigurationKeyValueListDataType]( + deviceConfiguration, model.FunctionTypeDeviceConfigurationKeyValueListData) + if err != nil || data == nil || data.DeviceConfigurationKeyValueData == nil { + return + } + + for _, item := range data.DeviceConfigurationKeyValueData { + if item.KeyId != nil && *item.KeyId == *description.KeyId { + keyData = item + break + } + } + + return +} + +func SetLocalDeviceConfigurationKeyValue( + service eebusapi.ServiceInterface, + keyName model.DeviceConfigurationKeyNameType, + changeable bool, + value model.DeviceConfigurationKeyValueValueType, +) (resultErr error) { + resultErr = eebusapi.ErrDataNotAvailable + + description := GetLocalDeviceConfigurationDescriptionForKeyName(service, keyName) + if description.KeyId == nil { + return + } + + localEntity := service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + deviceConfiguration := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + if deviceConfiguration == nil { + return + } + + data, err := spine.LocalFeatureDataCopyOfType[*model.DeviceConfigurationKeyValueListDataType](deviceConfiguration, model.FunctionTypeDeviceConfigurationKeyValueListData) + if err != nil { + data = &model.DeviceConfigurationKeyValueListDataType{} + } + + found := false + for index, item := range data.DeviceConfigurationKeyValueData { + if item.KeyId == nil || *item.KeyId != *description.KeyId { + continue + } + + item.IsValueChangeable = eebusutil.Ptr(changeable) + item.Value = eebusutil.Ptr(value) + + data.DeviceConfigurationKeyValueData[index] = item + found = true + } + + if !found { + item := model.DeviceConfigurationKeyValueDataType{ + KeyId: eebusutil.Ptr(*description.KeyId), + IsValueChangeable: eebusutil.Ptr(changeable), + Value: eebusutil.Ptr(value), + } + data.DeviceConfigurationKeyValueData = append(data.DeviceConfigurationKeyValueData, item) + } + + deviceConfiguration.SetData(model.FunctionTypeDeviceConfigurationKeyValueListData, data) + + return nil +} diff --git a/util/deviceconfiguration_test.go b/util/deviceconfiguration_test.go new file mode 100644 index 0000000..c950765 --- /dev/null +++ b/util/deviceconfiguration_test.go @@ -0,0 +1,218 @@ +package util + +import ( + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UtilSuite) Test_DeviceConfigurationCheckPayloadForKeyNameLocal() { + keyName := model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit + + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + + exists := DeviceConfigurationCheckDataPayloadForKeyName(true, s.service, payload, keyName) + assert.False(s.T(), exists) + + payload.Entity = s.monitoredEntity + + exists = DeviceConfigurationCheckDataPayloadForKeyName(true, s.service, payload, keyName) + assert.False(s.T(), exists) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: eebusutil.Ptr(keyName), + }, + }, + } + + entity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + feature := entity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + feature.SetData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData) + + exists = DeviceConfigurationCheckDataPayloadForKeyName(true, s.service, payload, keyName) + assert.False(s.T(), exists) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{}, + } + + payload.Data = keyData + + exists = DeviceConfigurationCheckDataPayloadForKeyName(true, s.service, payload, keyName) + assert.False(s.T(), exists) + + keyData = &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + String: eebusutil.Ptr(model.DeviceConfigurationKeyValueStringTypeIEC61851), + }, + }, + }, + } + + payload.Data = keyData + + exists = DeviceConfigurationCheckDataPayloadForKeyName(true, s.service, payload, keyName) + assert.True(s.T(), exists) +} + +func (s *UtilSuite) Test_DeviceConfigurationCheckPayloadForKeyName() { + keyName := model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit + + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + + exists := DeviceConfigurationCheckDataPayloadForKeyName(false, s.service, payload, keyName) + assert.False(s.T(), exists) + + payload.Entity = s.monitoredEntity + + exists = DeviceConfigurationCheckDataPayloadForKeyName(false, s.service, payload, keyName) + assert.False(s.T(), exists) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: eebusutil.Ptr(keyName), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + exists = DeviceConfigurationCheckDataPayloadForKeyName(false, s.service, payload, keyName) + assert.False(s.T(), exists) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{}, + } + + payload.Data = keyData + + exists = DeviceConfigurationCheckDataPayloadForKeyName(false, s.service, payload, keyName) + assert.False(s.T(), exists) + + keyData = &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + String: eebusutil.Ptr(model.DeviceConfigurationKeyValueStringTypeIEC61851), + }, + }, + }, + } + + payload.Data = keyData + + exists = DeviceConfigurationCheckDataPayloadForKeyName(false, s.service, payload, keyName) + assert.True(s.T(), exists) +} + +func (s *UtilSuite) Test_GetLocalDeviceConfigurationDescriptionForKeyName() { + keyName := model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit + + data := GetLocalDeviceConfigurationDescriptionForKeyName(s.service, keyName) + assert.Nil(s.T(), data.KeyId) + + entity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + feature := entity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: eebusutil.Ptr(keyName), + }, + }, + } + feature.SetData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData) + + data = GetLocalDeviceConfigurationDescriptionForKeyName(s.service, keyName) + assert.NotNil(s.T(), data.KeyId) +} + +func (s *UtilSuite) Test_GetLocalDeviceConfigurationKeyValueForId() { + keyName := model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit + + data := GetLocalDeviceConfigurationKeyValueForKeyName(s.service, keyName) + assert.Nil(s.T(), data.KeyId) + + entity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + feature := entity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: eebusutil.Ptr(keyName), + }, + }, + } + feature.SetData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData) + + data = GetLocalDeviceConfigurationKeyValueForKeyName(s.service, keyName) + assert.Nil(s.T(), data.KeyId) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + }, + }, + } + + feature.SetData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData) + + data = GetLocalDeviceConfigurationKeyValueForKeyName(s.service, keyName) + assert.NotNil(s.T(), data.KeyId) +} + +func (s *UtilSuite) Test_SetLocalDeviceConfigurationKeyValue() { + keyName := model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit + changeable := false + value := model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(10), + } + + err := SetLocalDeviceConfigurationKeyValue(s.service, keyName, changeable, value) + assert.NotNil(s.T(), err) + + entity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + feature := entity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: eebusutil.Ptr(keyName), + }, + }, + } + feature.SetData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData) + + err = SetLocalDeviceConfigurationKeyValue(s.service, keyName, changeable, value) + assert.Nil(s.T(), err) + + data := GetLocalDeviceConfigurationKeyValueForKeyName(s.service, keyName) + assert.NotNil(s.T(), data.KeyId) + assert.Equal(s.T(), uint(0), uint(*data.KeyId)) + assert.NotNil(s.T(), data.Value) + assert.NotNil(s.T(), data.Value.ScaledNumber) + assert.Equal(s.T(), 10.0, data.Value.ScaledNumber.GetValue()) + + err = SetLocalDeviceConfigurationKeyValue(s.service, keyName, true, value) + assert.Nil(s.T(), err) +} diff --git a/util/electricalconnection.go b/util/electricalconnection.go new file mode 100644 index 0000000..b41a055 --- /dev/null +++ b/util/electricalconnection.go @@ -0,0 +1,113 @@ +package util + +import ( + "github.com/enbility/cemd/api" + eebusapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +func GetPhaseCurrentLimits( + service eebusapi.ServiceInterface, + entity spineapi.EntityRemoteInterface, + entityTypes []model.EntityTypeType) ( + resultMin []float64, resultMax []float64, resultDefault []float64, resultErr error) { + if !IsCompatibleEntity(entity, entityTypes) { + return nil, nil, nil, api.ErrNoCompatibleEntity + } + + evElectricalConnection, err := ElectricalConnection(service, entity) + if err != nil { + return nil, nil, nil, eebusapi.ErrDataNotAvailable + } + + for _, phaseName := range PhaseNameMapping { + // electricalParameterDescription contains the measured phase for each measurementId + elParamDesc, err := evElectricalConnection.GetParameterDescriptionForMeasuredPhase(phaseName) + if err != nil || elParamDesc.ParameterId == nil { + continue + } + + dataMin, dataMax, dataDefault, err := 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. + + resultMin = append(resultMin, dataMin) + resultMax = append(resultMax, dataMax) + resultDefault = append(resultDefault, dataDefault) + } + + if len(resultMin) == 0 { + return nil, nil, nil, eebusapi.ErrDataNotAvailable + } + + return resultMin, resultMax, resultDefault, nil +} + +func GetLocalElectricalConnectionCharacteristicForContextType( + service eebusapi.ServiceInterface, + context model.ElectricalConnectionCharacteristicContextType, + charType model.ElectricalConnectionCharacteristicTypeType, +) (charData model.ElectricalConnectionCharacteristicDataType) { + charData = model.ElectricalConnectionCharacteristicDataType{} + + localEntity := service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + electricalConnection := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + if electricalConnection == nil { + return + } + + function := model.FunctionTypeElectricalConnectionCharacteristicListData + data, err := spine.LocalFeatureDataCopyOfType[*model.ElectricalConnectionCharacteristicListDataType]( + electricalConnection, function) + if err != nil || data == nil || data.ElectricalConnectionCharacteristicData == nil { + return + } + + for _, item := range data.ElectricalConnectionCharacteristicData { + if item.CharacteristicContext != nil && *item.CharacteristicContext == context && + item.CharacteristicType != nil && *item.CharacteristicType == charType { + charData = item + break + } + } + + return +} + +func SetLocalElectricalConnectionCharacteristicForContextType( + service eebusapi.ServiceInterface, + context model.ElectricalConnectionCharacteristicContextType, + charType model.ElectricalConnectionCharacteristicTypeType, + value float64, +) (resultErr error) { + resultErr = eebusapi.ErrDataNotAvailable + + charData := GetLocalElectricalConnectionCharacteristicForContextType(service, context, charType) + if charData.CharacteristicId == nil { + return + } + charData.Value = model.NewScaledNumberType(value) + + localEntity := service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + electricalConnection := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + if electricalConnection == nil { + return + } + function := model.FunctionTypeElectricalConnectionCharacteristicListData + + listData := &model.ElectricalConnectionCharacteristicListDataType{ + ElectricalConnectionCharacteristicData: []model.ElectricalConnectionCharacteristicDataType{charData}, + } + electricalConnection.SetData(function, listData) + + return nil +} diff --git a/util/electricalconnection_test.go b/util/electricalconnection_test.go new file mode 100644 index 0000000..1ff03b0 --- /dev/null +++ b/util/electricalconnection_test.go @@ -0,0 +1,228 @@ +package util + +import ( + "testing" + + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UtilSuite) Test_EVCurrentLimits() { + entityTypes := []model.EntityTypeType{model.EntityTypeTypeEV} + + minData, maxData, defaultData, err := GetPhaseCurrentLimits(s.service, s.mockRemoteEntity, entityTypes) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), minData) + assert.Nil(s.T(), maxData) + assert.Nil(s.T(), defaultData) + + minData, maxData, defaultData, err = GetPhaseCurrentLimits(s.service, s.monitoredEntity, entityTypes) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), minData) + assert.Nil(s.T(), maxData) + assert.Nil(s.T(), defaultData) + + minData, maxData, defaultData, err = GetPhaseCurrentLimits(s.service, s.monitoredEntity, entityTypes) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), minData) + assert.Nil(s.T(), maxData) + assert.Nil(s.T(), defaultData) + + paramData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(1)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(2)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeC), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramData, nil, nil) + assert.Nil(s.T(), fErr) + + minData, maxData, defaultData, err = GetPhaseCurrentLimits(s.service, s.monitoredEntity, entityTypes) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), minData) + assert.Nil(s.T(), maxData) + assert.Nil(s.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, 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, 16, 16}, + {true, 0.1, 0.1, 2, 2, 16, 16}, + {true, 0.1, 0.1, 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 { + s.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: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(index)), + PermittedValueSet: permittedData, + } + dataSet = append(dataSet, permittedItem) + } + + permData := &model.ElectricalConnectionPermittedValueSetListDataType{ + ElectricalConnectionPermittedValueSetData: dataSet, + } + + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionPermittedValueSetListData, permData, nil, nil) + assert.Nil(s.T(), fErr) + + minData, maxData, defaultData, err = GetPhaseCurrentLimits(s.service, s.monitoredEntity, entityTypes) + assert.Nil(s.T(), err) + + assert.Nil(s.T(), err) + assert.Equal(s.T(), len(tc.permitted), len(minData)) + assert.Equal(s.T(), len(tc.permitted), len(maxData)) + assert.Equal(s.T(), len(tc.permitted), len(defaultData)) + for index, item := range tc.permitted { + assert.Equal(s.T(), item.expectedMinValue, minData[index]) + assert.Equal(s.T(), item.expectedMaxValue, maxData[index]) + assert.Equal(s.T(), item.expectedDefaultValue, defaultData[index]) + } + }) + } +} + +func (s *UtilSuite) Test_GetLocalElectricalConnectionCharacteristicForContextType() { + context := model.ElectricalConnectionCharacteristicContextTypeEntity + charType := model.ElectricalConnectionCharacteristicTypeTypeApparentPowerConsumptionNominalMax + + data := GetLocalElectricalConnectionCharacteristicForContextType(s.service, context, charType) + assert.Nil(s.T(), data.CharacteristicId) + + entity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + feature := entity.FeatureOfTypeAndRole(model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + + charData := &model.ElectricalConnectionCharacteristicListDataType{ + ElectricalConnectionCharacteristicData: []model.ElectricalConnectionCharacteristicDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(0)), + CharacteristicId: eebusutil.Ptr(model.ElectricalConnectionCharacteristicIdType(0)), + CharacteristicContext: eebusutil.Ptr(context), + CharacteristicType: eebusutil.Ptr(charType), + }, + }, + } + feature.SetData(model.FunctionTypeElectricalConnectionCharacteristicListData, charData) + + data = GetLocalElectricalConnectionCharacteristicForContextType(s.service, context, charType) + assert.NotNil(s.T(), data.CharacteristicId) +} + +func (s *UtilSuite) Test_SetLocalElectricalConnectionCharacteristicForContextType() { + context := model.ElectricalConnectionCharacteristicContextTypeEntity + charType := model.ElectricalConnectionCharacteristicTypeTypeApparentPowerConsumptionNominalMax + value := 10.0 + + err := SetLocalElectricalConnectionCharacteristicForContextType(s.service, context, charType, value) + assert.NotNil(s.T(), err) + + entity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + feature := entity.FeatureOfTypeAndRole(model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + + charData := &model.ElectricalConnectionCharacteristicListDataType{ + ElectricalConnectionCharacteristicData: []model.ElectricalConnectionCharacteristicDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(0)), + CharacteristicId: eebusutil.Ptr(model.ElectricalConnectionCharacteristicIdType(0)), + CharacteristicContext: eebusutil.Ptr(context), + CharacteristicType: eebusutil.Ptr(charType), + }, + }, + } + feature.SetData(model.FunctionTypeElectricalConnectionCharacteristicListData, charData) + + err = SetLocalElectricalConnectionCharacteristicForContextType(s.service, context, charType, value) + assert.Nil(s.T(), err) + + data := GetLocalElectricalConnectionCharacteristicForContextType(s.service, context, charType) + assert.NotNil(s.T(), data.CharacteristicId) + assert.Equal(s.T(), uint(0), uint(*data.CharacteristicId)) + assert.NotNil(s.T(), data.Value) + assert.Equal(s.T(), 10.0, data.Value.GetValue()) +} diff --git a/util/features.go b/util/features.go new file mode 100644 index 0000000..766443b --- /dev/null +++ b/util/features.go @@ -0,0 +1,76 @@ +package util + +import ( + eebusapi "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features" + "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +func localCemEntity(service eebusapi.ServiceInterface) api.EntityLocalInterface { + localDevice := service.LocalDevice() + + localEntity := localDevice.EntityForType(model.EntityTypeTypeCEM) + + return localEntity +} + +func DeviceClassification(service eebusapi.ServiceInterface, remoteEntity api.EntityRemoteInterface) (*features.DeviceClassification, error) { + localEntity := localCemEntity(service) + + return features.NewDeviceClassification(localEntity, remoteEntity) +} + +func DeviceConfiguration(service eebusapi.ServiceInterface, remoteEntity api.EntityRemoteInterface) (*features.DeviceConfiguration, error) { + localEntity := localCemEntity(service) + + return features.NewDeviceConfiguration(localEntity, remoteEntity) +} + +func DeviceDiagnosis(service eebusapi.ServiceInterface, remoteEntity api.EntityRemoteInterface) (*features.DeviceDiagnosis, error) { + localEntity := localCemEntity(service) + + return features.NewDeviceDiagnosis(localEntity, remoteEntity) +} + +func DeviceDiagnosisServer(service eebusapi.ServiceInterface, remoteEntity api.EntityRemoteInterface) (*features.DeviceDiagnosis, error) { + localEntity := localCemEntity(service) + + return features.NewDeviceDiagnosis(localEntity, remoteEntity) +} + +func ElectricalConnection(service eebusapi.ServiceInterface, remoteEntity api.EntityRemoteInterface) (*features.ElectricalConnection, error) { + localEntity := localCemEntity(service) + + return features.NewElectricalConnection(localEntity, remoteEntity) +} + +func Identification(service eebusapi.ServiceInterface, remoteEntity api.EntityRemoteInterface) (*features.Identification, error) { + localEntity := localCemEntity(service) + + return features.NewIdentification(localEntity, remoteEntity) +} + +func Measurement(service eebusapi.ServiceInterface, remoteEntity api.EntityRemoteInterface) (*features.Measurement, error) { + localEntity := localCemEntity(service) + + return features.NewMeasurement(localEntity, remoteEntity) +} + +func LoadControl(service eebusapi.ServiceInterface, remoteEntity api.EntityRemoteInterface) (*features.LoadControl, error) { + localEntity := localCemEntity(service) + + return features.NewLoadControl(localEntity, remoteEntity) +} + +func TimeSeries(service eebusapi.ServiceInterface, remoteEntity api.EntityRemoteInterface) (*features.TimeSeries, error) { + localEntity := localCemEntity(service) + + return features.NewTimeSeries(localEntity, remoteEntity) +} + +func IncentiveTable(service eebusapi.ServiceInterface, remoteEntity api.EntityRemoteInterface) (*features.IncentiveTable, error) { + localEntity := localCemEntity(service) + + return features.NewIncentiveTable(localEntity, remoteEntity) +} diff --git a/util/features_test.go b/util/features_test.go new file mode 100644 index 0000000..f7072b1 --- /dev/null +++ b/util/features_test.go @@ -0,0 +1,53 @@ +package util + +import "github.com/stretchr/testify/assert" + +func (s *UtilSuite) Test_Features() { + feature1, err := DeviceClassification(s.service, s.evseEntity) + assert.Nil(s.T(), feature1) + assert.NotNil(s.T(), err) + + feature11, err := DeviceClassification(s.service, s.monitoredEntity) + assert.NotNil(s.T(), feature11) + assert.Nil(s.T(), err) + + feature2, err := DeviceConfiguration(s.service, s.evseEntity) + assert.Nil(s.T(), feature2) + assert.NotNil(s.T(), err) + + feature21, err := DeviceConfiguration(s.service, s.monitoredEntity) + assert.NotNil(s.T(), feature21) + assert.Nil(s.T(), err) + + feature3, err := DeviceDiagnosis(s.service, s.monitoredEntity) + assert.Nil(s.T(), feature3) + assert.NotNil(s.T(), err) + + feature4, err := DeviceDiagnosisServer(s.service, s.monitoredEntity) + assert.Nil(s.T(), feature4) + assert.NotNil(s.T(), err) + + feature5, err := ElectricalConnection(s.service, s.evseEntity) + assert.Nil(s.T(), feature5) + assert.NotNil(s.T(), err) + + feature6, err := Identification(s.service, s.monitoredEntity) + assert.Nil(s.T(), feature6) + assert.NotNil(s.T(), err) + + feature7, err := Measurement(s.service, s.evseEntity) + assert.Nil(s.T(), feature7) + assert.NotNil(s.T(), err) + + feature8, err := LoadControl(s.service, s.evseEntity) + assert.Nil(s.T(), feature8) + assert.NotNil(s.T(), err) + + feature9, err := TimeSeries(s.service, s.monitoredEntity) + assert.Nil(s.T(), feature9) + assert.NotNil(s.T(), err) + + feature10, err := IncentiveTable(s.service, s.monitoredEntity) + assert.Nil(s.T(), feature10) + assert.NotNil(s.T(), err) +} diff --git a/util/helper.go b/util/helper.go index b2f7b04..459a4a6 100644 --- a/util/helper.go +++ b/util/helper.go @@ -1,41 +1,57 @@ package util 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" + "slices" + + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/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() - for _, element := range uci { - if *element.Actor != actor { - continue - } - for _, uc := range element.UseCaseSupport { - if uc.UseCaseName != nil && *uc.UseCaseName == usecase { - return true - } - } +func IsCompatibleEntity(entity spineapi.EntityRemoteInterface, entityTypes []model.EntityTypeType) bool { + if entity == nil { + return false } - return false + return slices.Contains(entityTypes, entity.EntityType()) +} + +func IsDeviceConnected(payload spineapi.EventPayload) bool { + return payload.Device != nil && + payload.EventType == spineapi.EventTypeDeviceChange && + payload.ChangeType == spineapi.ElementChangeAdd } -// return the remote entity of a given type and device ski -func EntityOfTypeForSki(service *service.EEBUSService, entityType model.EntityTypeType, ski string) (*spine.EntityRemoteImpl, error) { - rDevice := service.RemoteDeviceForSki(ski) +func IsDeviceDisconnected(payload spineapi.EventPayload) bool { + return payload.Device != nil && + payload.EventType == spineapi.EventTypeDeviceChange && + payload.ChangeType == spineapi.ElementChangeRemove +} + +func IsEntityConnected(payload spineapi.EventPayload) bool { + if payload.Entity != nil && + payload.EventType == spineapi.EventTypeEntityChange && + payload.ChangeType == spineapi.ElementChangeAdd { + return true + } + + return false +} - entities := rDevice.Entities() - for _, entity := range entities { - if entity.EntityType() == entityType { - return entity, nil - } +func IsEntityDisconnected(payload spineapi.EventPayload) bool { + if payload.Entity != nil && + payload.EventType == spineapi.EventTypeEntityChange && + payload.ChangeType == spineapi.ElementChangeRemove { + return true } - return nil, features.ErrEntityNotFound + return false +} + +func Deref(v *string) string { + if v != nil { + return string(*v) + } + return "" } diff --git a/util/helper_test.go b/util/helper_test.go new file mode 100644 index 0000000..fb325ea --- /dev/null +++ b/util/helper_test.go @@ -0,0 +1,85 @@ +package util + +import ( + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UtilSuite) Test_IsCompatibleEntity() { + payload := spineapi.EventPayload{} + validEntityTypes := []model.EntityTypeType{model.EntityTypeTypeEV} + result := IsCompatibleEntity(payload.Entity, validEntityTypes) + assert.Equal(s.T(), false, result) + + payload = spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + result = IsCompatibleEntity(payload.Entity, validEntityTypes) + assert.Equal(s.T(), false, result) + + payload = spineapi.EventPayload{ + Entity: s.monitoredEntity, + } + result = IsCompatibleEntity(payload.Entity, validEntityTypes) + assert.Equal(s.T(), true, result) +} + +func (s *UtilSuite) Test_IsDeviceConnected() { + payload := spineapi.EventPayload{} + result := IsDeviceConnected(payload) + assert.Equal(s.T(), false, result) + + device := mocks.NewDeviceRemoteInterface(s.T()) + payload = spineapi.EventPayload{ + Device: device, + EventType: spineapi.EventTypeDeviceChange, + ChangeType: spineapi.ElementChangeAdd, + } + result = IsDeviceConnected(payload) + assert.Equal(s.T(), true, result) +} + +func (s *UtilSuite) Test_IsDeviceDisconnected() { + payload := spineapi.EventPayload{} + result := IsDeviceDisconnected(payload) + assert.Equal(s.T(), false, result) + + device := mocks.NewDeviceRemoteInterface(s.T()) + payload = spineapi.EventPayload{ + Device: device, + EventType: spineapi.EventTypeDeviceChange, + ChangeType: spineapi.ElementChangeRemove, + } + result = IsDeviceDisconnected(payload) + assert.Equal(s.T(), true, result) +} + +func (s *UtilSuite) Test_IsEntityConnected() { + payload := spineapi.EventPayload{} + result := IsEntityConnected(payload) + assert.Equal(s.T(), false, result) + + payload = spineapi.EventPayload{ + Entity: s.evseEntity, + EventType: spineapi.EventTypeEntityChange, + ChangeType: spineapi.ElementChangeAdd, + } + result = IsEntityConnected(payload) + assert.Equal(s.T(), true, result) +} + +func (s *UtilSuite) Test_IsEntityDisconnected() { + payload := spineapi.EventPayload{} + result := IsEntityDisconnected(payload) + assert.Equal(s.T(), false, result) + + payload = spineapi.EventPayload{ + Entity: s.evseEntity, + EventType: spineapi.EventTypeEntityChange, + ChangeType: spineapi.ElementChangeRemove, + } + result = IsEntityDisconnected(payload) + assert.Equal(s.T(), true, result) +} diff --git a/util/loadcontrol.go b/util/loadcontrol.go new file mode 100644 index 0000000..c0a6b13 --- /dev/null +++ b/util/loadcontrol.go @@ -0,0 +1,314 @@ +package util + +import ( + "github.com/enbility/cemd/api" + eebusapi "github.com/enbility/eebus-go/api" + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +// Check the payload data if it contains measurementId values for a given scope +func LoadControlLimitsCheckPayloadDataForTypeCategoryDirectionScope( + localServer bool, + service eebusapi.ServiceInterface, + payload spineapi.EventPayload, + limitType model.LoadControlLimitTypeType, + limitCategory model.LoadControlCategoryType, + direction model.EnergyDirectionType, + scope model.ScopeTypeType) bool { + var descs []model.LoadControlLimitDescriptionDataType + + if payload.Data == nil { + return false + } + + limits := payload.Data.(*model.LoadControlLimitListDataType) + + if localServer { + descs = GetLocalLimitDescriptionsForTypeCategoryDirectionScope(service, limitType, limitCategory, direction, scope) + } else { + loadcontrolF, err := LoadControl(service, payload.Entity) + if err != nil { + return false + } + + descs, err = loadcontrolF.GetLimitDescriptionsForTypeCategoryDirectionScope(limitType, limitCategory, direction, scope) + if err != nil { + return false + } + } + + for _, item := range descs { + if item.LimitId == nil { + continue + } + + for _, limit := range limits.LoadControlLimitData { + if limit.LimitId != nil && + *limit.LimitId == *item.LimitId && + limit.Value != nil { + return true + } + } + } + + return false +} + +// return the current loadcontrol limits for a categoriy +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func LoadControlLimits( + service eebusapi.ServiceInterface, + entity spineapi.EntityRemoteInterface, + entityTypes []model.EntityTypeType, + limitType model.LoadControlLimitTypeType, + limitCategory model.LoadControlCategoryType, + scopeType model.ScopeTypeType) (limits []api.LoadLimitsPhase, resultErr error) { + limits = nil + resultErr = api.ErrNoCompatibleEntity + if entity == nil || !IsCompatibleEntity(entity, entityTypes) { + return + } + + evLoadControl, err := LoadControl(service, entity) + evElectricalConnection, err2 := ElectricalConnection(service, entity) + if err != nil || err2 != nil { + return + } + + resultErr = eebusapi.ErrDataNotAvailable + // find out the appropriate limitId for each phase value + // limitDescription contains the measurementId for each limitId + limitDescriptions, err := evLoadControl.GetLimitDescriptionsForTypeCategoryDirectionScope( + limitType, limitCategory, "", scopeType) + if err != nil { + return + } + + var result []api.LoadLimitsPhase + + for i := 0; i < len(PhaseNameMapping); i++ { + phaseName := PhaseNameMapping[i] + + // electricalParameterDescription contains the measured phase for each measurementId + elParamDesc, err := evElectricalConnection.GetParameterDescriptionForMeasuredPhase(phaseName) + if err != nil || elParamDesc.MeasurementId == nil { + // there is no data for this phase, the phase may not exist + result = append(result, api.LoadLimitsPhase{Phase: phaseName}) + continue + } + + var limitDesc *model.LoadControlLimitDescriptionDataType + for _, desc := range limitDescriptions { + if desc.MeasurementId != nil && + elParamDesc.MeasurementId != nil && + *desc.MeasurementId == *elParamDesc.MeasurementId { + safeDesc := desc + limitDesc = &safeDesc + break + } + } + + if limitDesc == nil || limitDesc.LimitId == nil { + return + } + + limitIdData, err := evLoadControl.GetLimitValueForLimitId(*limitDesc.LimitId) + if err != nil { + return + } + + var limitValue float64 + if limitIdData.Value == nil || (limitIdData.IsLimitActive != nil && !*limitIdData.IsLimitActive) { + // report maximum possible if no limit is available or the limit is not active + _, dataMax, _, err := evElectricalConnection.GetLimitsForParameterId(*elParamDesc.ParameterId) + if err != nil { + return + } + + limitValue = dataMax + } else { + limitValue = limitIdData.Value.GetValue() + } + + newLimit := api.LoadLimitsPhase{ + Phase: phaseName, + IsChangeable: (limitIdData.IsLimitChangeable != nil && *limitIdData.IsLimitChangeable), + IsActive: (limitIdData.IsLimitActive != nil && *limitIdData.IsLimitActive), + Value: limitValue, + } + + result = append(result, newLimit) + } + + return result, nil +} + +// generic helper to be used in UCOPEV & UCOSCEV +// send new LoadControlLimits to the remote EV +// +// parameters: +// - limits: a set of limits for a given limit category containing phase specific limit data +// +// category obligations: +// Sets a maximum A limit for each phase that the EV may not exceed. +// Mainly used for implementing overload protection of the site or limiting the +// maximum charge power of EVs when the EV and EVSE communicate via IEC61851 +// and with ISO15118 if the EV does not support the Optimization of Self Consumption +// usecase. +// +// category recommendations: +// Sets a recommended charge power in A for each phase. This is mainly +// used if the EV and EVSE communicate via ISO15118 to support charging excess solar power. +// The EV either needs to support the Optimization of Self Consumption usecase or +// the EVSE needs to be able map the recommendations into oligation limits which then +// works for all EVs communication either via IEC61851 or ISO15118. +// +// 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 WriteLoadControlLimits( + service eebusapi.ServiceInterface, + entity spineapi.EntityRemoteInterface, + entityTypes []model.EntityTypeType, + category model.LoadControlCategoryType, + limits []api.LoadLimitsPhase) (*model.MsgCounterType, error) { + if entity == nil || !IsCompatibleEntity(entity, entityTypes) { + return nil, api.ErrNoCompatibleEntity + } + + loadControl, err := LoadControl(service, entity) + electricalConnection, err2 := ElectricalConnection(service, entity) + if err != nil || err2 != nil { + return nil, api.ErrNoCompatibleEntity + } + + var limitData []model.LoadControlLimitDataType + + for _, phaseLimit := range limits { + // find out the appropriate limitId for each phase value + // limitDescription contains the measurementId for each limitId + limitDescriptions, err := loadControl.GetLimitDescriptionsForCategory(category) + if err != nil { + continue + } + + // electricalParameterDescription contains the measured phase for each measurementId + elParamDesc, err := electricalConnection.GetParameterDescriptionForMeasuredPhase(phaseLimit.Phase) + if err != nil || elParamDesc.MeasurementId == nil { + continue + } + + var limitDesc *model.LoadControlLimitDescriptionDataType + for _, desc := range limitDescriptions { + if desc.MeasurementId != nil && + elParamDesc.MeasurementId != nil && + *desc.MeasurementId == *elParamDesc.MeasurementId { + safeDesc := desc + limitDesc = &safeDesc + break + } + } + + if limitDesc == nil || limitDesc.LimitId == nil { + continue + } + + limitIdData, err := loadControl.GetLimitValueForLimitId(*limitDesc.LimitId) + if err != nil { + continue + } + + // 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 + } + + // electricalPermittedValueSet contains the allowed min, max and the default values per phase + limit := electricalConnection.AdjustValueToBeWithinPermittedValuesForParameter(phaseLimit.Value, *elParamDesc.ParameterId) + + newLimit := model.LoadControlLimitDataType{ + LimitId: limitDesc.LimitId, + IsLimitActive: eebusutil.Ptr(phaseLimit.IsActive), + Value: model.NewScaledNumberType(limit), + } + limitData = append(limitData, newLimit) + } + + msgCounter, err := loadControl.WriteLimitValues(limitData) + + return msgCounter, err +} + +func GetLocalLimitDescriptionsForTypeCategoryDirectionScope( + service eebusapi.ServiceInterface, + limitType model.LoadControlLimitTypeType, + limitCategory model.LoadControlCategoryType, + limitDirection model.EnergyDirectionType, + scopeType model.ScopeTypeType, +) (descriptions []model.LoadControlLimitDescriptionDataType) { + descriptions = []model.LoadControlLimitDescriptionDataType{} + + localEntity := service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + loadControl := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + if loadControl == nil { + return + } + + data, err := spine.LocalFeatureDataCopyOfType[*model.LoadControlLimitDescriptionListDataType]( + loadControl, model.FunctionTypeLoadControlLimitDescriptionListData) + if err != nil || data == nil || data.LoadControlLimitDescriptionData == nil { + return + } + + for _, desc := range data.LoadControlLimitDescriptionData { + if desc.LimitId != nil && + (limitType == "" || (desc.LimitType != nil && *desc.LimitType == limitType)) && + (limitCategory == "" || (desc.LimitCategory != nil && *desc.LimitCategory == limitCategory)) && + (limitDirection == "" || (desc.LimitDirection != nil && *desc.LimitDirection == limitDirection)) && + (scopeType == "" || (desc.ScopeType != nil && *desc.ScopeType == scopeType)) { + descriptions = append(descriptions, desc) + } + } + + return descriptions +} + +func GetLocalLimitValueForLimitId( + service eebusapi.ServiceInterface, + limitId model.LoadControlLimitIdType, +) (value model.LoadControlLimitDataType) { + value = model.LoadControlLimitDataType{} + + localEntity := service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + loadControl := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + if loadControl == nil { + return + } + + values, err := spine.LocalFeatureDataCopyOfType[*model.LoadControlLimitListDataType]( + loadControl, model.FunctionTypeLoadControlLimitListData) + if err != nil || values == nil || values.LoadControlLimitData == nil { + return + } + + for _, item := range values.LoadControlLimitData { + if item.LimitId != nil && *item.LimitId == limitId { + value = item + break + } + } + + return +} diff --git a/util/loadcontrol_test.go b/util/loadcontrol_test.go new file mode 100644 index 0000000..1b8bc1c --- /dev/null +++ b/util/loadcontrol_test.go @@ -0,0 +1,646 @@ +package util + +import ( + "testing" + + "github.com/enbility/cemd/api" + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UtilSuite) Test_LoadControlLimitsCheckPayloadDataForTypeCategoryDirectionScopeLocal() { + limitType := model.LoadControlLimitTypeTypeMaxValueLimit + scope := model.ScopeTypeTypeSelfConsumption + category := model.LoadControlCategoryTypeObligation + direction := model.EnergyDirectionType("") + + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + + exists := LoadControlLimitsCheckPayloadDataForTypeCategoryDirectionScope(true, s.service, payload, limitType, category, direction, scope) + assert.False(s.T(), exists) + + payload.Entity = s.monitoredEntity + + exists = LoadControlLimitsCheckPayloadDataForTypeCategoryDirectionScope(true, s.service, payload, limitType, category, direction, scope) + assert.False(s.T(), exists) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + LimitCategory: eebusutil.Ptr(category), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + LimitType: eebusutil.Ptr(limitType), + ScopeType: eebusutil.Ptr(scope), + }, + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(1)), + LimitCategory: eebusutil.Ptr(category), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + LimitType: eebusutil.Ptr(limitType), + ScopeType: eebusutil.Ptr(scope), + }, + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(2)), + LimitCategory: eebusutil.Ptr(category), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + LimitType: eebusutil.Ptr(limitType), + ScopeType: eebusutil.Ptr(scope), + }, + }, + } + + entity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + feature := entity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + feature.SetData(model.FunctionTypeLoadControlLimitDescriptionListData, descData) + + exists = LoadControlLimitsCheckPayloadDataForTypeCategoryDirectionScope(true, s.service, payload, limitType, category, direction, scope) + assert.False(s.T(), exists) + + paramData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(1)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(2)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeC), + }, + }, + } + + elFeature := entity.FeatureOfTypeAndRole(model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + elFeature.SetData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramData) + + exists = LoadControlLimitsCheckPayloadDataForTypeCategoryDirectionScope(true, s.service, payload, limitType, category, direction, scope) + assert.False(s.T(), exists) + + limitData := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{}, + } + + payload.Data = limitData + exists = LoadControlLimitsCheckPayloadDataForTypeCategoryDirectionScope(true, s.service, payload, limitType, category, direction, scope) + assert.False(s.T(), exists) + + limitData = &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + Value: model.NewScaledNumberType(16), + }, + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(1)), + Value: model.NewScaledNumberType(16), + }, + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(2)), + }, + }, + } + + payload.Data = limitData + exists = LoadControlLimitsCheckPayloadDataForTypeCategoryDirectionScope(true, s.service, payload, limitType, category, direction, scope) + assert.True(s.T(), exists) +} + +func (s *UtilSuite) Test_LoadControlLimitsCheckPayloadDataForTypeCategoryDirectionScope() { + limitType := model.LoadControlLimitTypeTypeMaxValueLimit + scope := model.ScopeTypeTypeSelfConsumption + category := model.LoadControlCategoryTypeObligation + direction := model.EnergyDirectionType("") + + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + + exists := LoadControlLimitsCheckPayloadDataForTypeCategoryDirectionScope(false, s.service, payload, limitType, category, direction, scope) + assert.False(s.T(), exists) + + payload.Entity = s.monitoredEntity + + exists = LoadControlLimitsCheckPayloadDataForTypeCategoryDirectionScope(false, s.service, payload, limitType, category, direction, scope) + assert.False(s.T(), exists) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + LimitCategory: eebusutil.Ptr(category), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + LimitType: eebusutil.Ptr(limitType), + ScopeType: eebusutil.Ptr(scope), + }, + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(1)), + LimitCategory: eebusutil.Ptr(category), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + LimitType: eebusutil.Ptr(limitType), + ScopeType: eebusutil.Ptr(scope), + }, + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(2)), + LimitCategory: eebusutil.Ptr(category), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + LimitType: eebusutil.Ptr(limitType), + ScopeType: eebusutil.Ptr(scope), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeLoadControlLimitDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + exists = LoadControlLimitsCheckPayloadDataForTypeCategoryDirectionScope(false, s.service, payload, limitType, category, direction, scope) + assert.False(s.T(), exists) + + paramData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(1)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(2)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeC), + }, + }, + } + + rElFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramData, nil, nil) + assert.Nil(s.T(), fErr) + + exists = LoadControlLimitsCheckPayloadDataForTypeCategoryDirectionScope(false, s.service, payload, limitType, category, direction, scope) + assert.False(s.T(), exists) + + limitData := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{}, + } + + payload.Data = limitData + exists = LoadControlLimitsCheckPayloadDataForTypeCategoryDirectionScope(false, s.service, payload, limitType, category, direction, scope) + assert.False(s.T(), exists) + + limitData = &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + Value: model.NewScaledNumberType(16), + }, + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(1)), + Value: model.NewScaledNumberType(16), + }, + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(2)), + }, + }, + } + + payload.Data = limitData + exists = LoadControlLimitsCheckPayloadDataForTypeCategoryDirectionScope(false, s.service, payload, limitType, category, direction, scope) + assert.True(s.T(), exists) +} + +func (s *UtilSuite) Test_LoadControlLimits() { + var data []api.LoadLimitsPhase + var err error + limitType := model.LoadControlLimitTypeTypeMaxValueLimit + scope := model.ScopeTypeTypeSelfConsumption + category := model.LoadControlCategoryTypeObligation + entityTypes := []model.EntityTypeType{model.EntityTypeTypeEV} + + data, err = LoadControlLimits(s.service, s.mockRemoteEntity, entityTypes, limitType, category, scope) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + data, err = LoadControlLimits(s.service, s.monitoredEntity, entityTypes, limitType, category, scope) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + LimitCategory: eebusutil.Ptr(category), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + LimitType: eebusutil.Ptr(limitType), + ScopeType: eebusutil.Ptr(scope), + }, + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(1)), + LimitCategory: eebusutil.Ptr(category), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + LimitType: eebusutil.Ptr(limitType), + ScopeType: eebusutil.Ptr(scope), + }, + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(2)), + LimitCategory: eebusutil.Ptr(category), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + LimitType: eebusutil.Ptr(limitType), + ScopeType: eebusutil.Ptr(scope), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeLoadControlLimitDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = LoadControlLimits(s.service, s.monitoredEntity, entityTypes, limitType, category, scope) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 3, len(data)) + assert.Equal(s.T(), 0.0, data[0].Value) + + paramData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(1)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(2)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeC), + }, + }, + } + + rElFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = LoadControlLimits(s.service, s.monitoredEntity, entityTypes, limitType, category, scope) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + limitData := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + Value: model.NewScaledNumberType(16), + }, + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(1)), + Value: model.NewScaledNumberType(16), + }, + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(2)), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeLoadControlLimitListData, limitData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = LoadControlLimits(s.service, s.monitoredEntity, entityTypes, limitType, category, scope) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + permData := &model.ElectricalConnectionPermittedValueSetListDataType{ + ElectricalConnectionPermittedValueSetData: []model.ElectricalConnectionPermittedValueSetDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(2)), + PermittedValueSet: []model.ScaledNumberSetType{ + { + Value: []model.ScaledNumberType{ + *model.NewScaledNumberType(0), + }, + Range: []model.ScaledNumberRangeType{ + { + Min: model.NewScaledNumberType(6), + Max: model.NewScaledNumberType(16), + }, + }, + }, + }, + }, + }, + } + + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionPermittedValueSetListData, permData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = LoadControlLimits(s.service, s.monitoredEntity, entityTypes, limitType, category, scope) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 3, len(data)) + assert.Equal(s.T(), 16.0, data[0].Value) +} + +func (s *UtilSuite) Test_WriteLoadControlLimits() { + loadLimits := []api.LoadLimitsPhase{} + + category := model.LoadControlCategoryTypeObligation + entityTypes := []model.EntityTypeType{model.EntityTypeTypeEV} + + msgCounter, err := WriteLoadControlLimits(s.service, s.mockRemoteEntity, entityTypes, category, loadLimits) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), msgCounter) + + msgCounter, err = WriteLoadControlLimits(s.service, s.monitoredEntity, entityTypes, category, loadLimits) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), msgCounter) + + paramData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(1)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(2)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeC), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramData, nil, nil) + assert.Nil(s.T(), fErr) + + msgCounter, err = WriteLoadControlLimits(s.service, s.monitoredEntity, entityTypes, category, loadLimits) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), msgCounter) + + type dataStruct struct { + phases int + permittedDefaultExists bool + permittedDefaultValue float64 + permittedMinValue float64 + permittedMaxValue float64 + limits, limitsExpected []float64 + } + + tests := []struct { + name string + data []dataStruct + }{ + { + "1 Phase ISO15118", + []dataStruct{ + {1, true, 0.1, 2, 16, []float64{0}, []float64{0.1}}, + {1, true, 0.1, 2, 16, []float64{2.2}, []float64{2.2}}, + {1, true, 0.1, 2, 16, []float64{10}, []float64{10}}, + {1, true, 0.1, 2, 16, []float64{16}, []float64{16}}, + }, + }, + { + "3 Phase ISO15118", + []dataStruct{ + {3, true, 0.1, 2, 16, []float64{0, 0, 0}, []float64{0.1, 0.1, 0.1}}, + {3, true, 0.1, 2, 16, []float64{2.2, 2.2, 2.2}, []float64{2.2, 2.2, 2.2}}, + {3, true, 0.1, 2, 16, []float64{10, 10, 10}, []float64{10, 10, 10}}, + {3, true, 0.1, 2, 16, []float64{16, 16, 16}, []float64{16, 16, 16}}, + }, + }, + { + "1 Phase IEC61851", + []dataStruct{ + {1, true, 0, 6, 16, []float64{0}, []float64{0}}, + {1, true, 0, 6, 16, []float64{6}, []float64{6}}, + {1, true, 0, 6, 16, []float64{10}, []float64{10}}, + {1, true, 0, 6, 16, []float64{16}, []float64{16}}, + }, + }, + { + "3 Phase IEC61851", + []dataStruct{ + {3, true, 0, 6, 16, []float64{0, 0, 0}, []float64{0, 0, 0}}, + {3, true, 0, 6, 16, []float64{6, 6, 6}, []float64{6, 6, 6}}, + {3, true, 0, 6, 16, []float64{10, 10, 10}, []float64{10, 10, 10}}, + {3, true, 0, 6, 16, []float64{16, 16, 16}, []float64{16, 16, 16}}, + }, + }, + { + "3 Phase IEC61851 Elli", + []dataStruct{ + {3, false, 0, 6, 16, []float64{0, 0, 0}, []float64{0, 0, 0}}, + {3, false, 0, 6, 16, []float64{6, 6, 6}, []float64{6, 6, 6}}, + {3, false, 0, 6, 16, []float64{10, 10, 10}, []float64{10, 10, 10}}, + {3, false, 0, 6, 16, []float64{16, 16, 16}, []float64{16, 16, 16}}, + }, + }, + } + + for _, tc := range tests { + s.T().Run(tc.name, func(t *testing.T) { + dataSet := []model.ElectricalConnectionPermittedValueSetDataType{} + permittedData := []model.ScaledNumberSetType{} + for _, data := range tc.data { + // clean up data + remoteLoadControlF := s.monitoredEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + assert.NotNil(s.T(), remoteLoadControlF) + + emptyLimits := model.LoadControlLimitListDataType{} + errT := remoteLoadControlF.UpdateData(model.FunctionTypeLoadControlLimitListData, &emptyLimits, nil, nil) + assert.Nil(s.T(), errT) + + 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: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(phase)), + PermittedValueSet: permittedData, + } + dataSet = append(dataSet, permittedItem) + } + + permData := &model.ElectricalConnectionPermittedValueSetListDataType{ + ElectricalConnectionPermittedValueSetData: dataSet, + } + + fErr = rFeature.UpdateData(model.FunctionTypeElectricalConnectionPermittedValueSetListData, permData, nil, nil) + assert.Nil(s.T(), fErr) + + msgCounter, err := WriteLoadControlLimits(s.service, s.monitoredEntity, entityTypes, category, loadLimits) + assert.NotNil(t, err) + assert.Nil(t, msgCounter) + + limitDesc := []model.LoadControlLimitDescriptionDataType{} + for index := range data.limits { + id := model.LoadControlLimitIdType(index) + limitItem := model.LoadControlLimitDescriptionDataType{ + LimitId: eebusutil.Ptr(id), + LimitCategory: eebusutil.Ptr(model.LoadControlCategoryTypeObligation), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(index)), + } + limitDesc = append(limitDesc, limitItem) + } + add := len(limitDesc) + for index := range data.limits { + id := model.LoadControlLimitIdType(index + add) + limitItem := model.LoadControlLimitDescriptionDataType{ + LimitId: eebusutil.Ptr(id), + LimitCategory: eebusutil.Ptr(model.LoadControlCategoryTypeRecommendation), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(index)), + } + limitDesc = append(limitDesc, limitItem) + } + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: limitDesc, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + fErr = rFeature.UpdateData(model.FunctionTypeLoadControlLimitDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + msgCounter, err = WriteLoadControlLimits(s.service, s.monitoredEntity, entityTypes, category, loadLimits) + assert.NotNil(t, err) + assert.Nil(t, msgCounter) + + limitData := []model.LoadControlLimitDataType{} + for index := range limitDesc { + limitItem := model.LoadControlLimitDataType{ + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(index)), + IsLimitChangeable: eebusutil.Ptr(true), + IsLimitActive: eebusutil.Ptr(false), + Value: model.NewScaledNumberType(data.permittedMaxValue), + } + limitData = append(limitData, limitItem) + } + + limitListData := &model.LoadControlLimitListDataType{ + LoadControlLimitData: limitData, + } + + fErr = rFeature.UpdateData(model.FunctionTypeLoadControlLimitListData, limitListData, nil, nil) + assert.Nil(s.T(), fErr) + + msgCounter, err = WriteLoadControlLimits(s.service, s.monitoredEntity, entityTypes, category, loadLimits) + assert.NotNil(t, err) + assert.Nil(t, msgCounter) + + phaseLimitValues := []api.LoadLimitsPhase{} + for index, limit := range data.limits { + phase := PhaseNameMapping[index] + phaseLimitValues = append(phaseLimitValues, api.LoadLimitsPhase{ + Phase: phase, + IsActive: true, + Value: limit, + }) + } + + msgCounter, err = WriteLoadControlLimits(s.service, s.monitoredEntity, entityTypes, category, phaseLimitValues) + assert.Nil(t, err) + assert.NotNil(t, msgCounter) + + msgCounter, err = WriteLoadControlLimits(s.service, s.monitoredEntity, entityTypes, category, phaseLimitValues) + assert.Nil(t, err) + assert.NotNil(t, msgCounter) + } + }) + } +} + +func (s *UtilSuite) Test_GetLocalLimitDescriptionsForTypeCategoryDirectionScope() { + limitType := model.LoadControlLimitTypeTypeSignDependentAbsValueLimit + limitCategory := model.LoadControlCategoryTypeObligation + limitDirection := model.EnergyDirectionTypeConsume + limitScopeType := model.ScopeTypeTypeActivePowerLimit + + data := GetLocalLimitDescriptionsForTypeCategoryDirectionScope(s.service, limitType, limitCategory, limitDirection, limitScopeType) + assert.Equal(s.T(), 0, len(data)) + + entity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + feature := entity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + + desc := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + LimitType: eebusutil.Ptr(limitType), + LimitCategory: eebusutil.Ptr(limitCategory), + LimitDirection: eebusutil.Ptr(limitDirection), + ScopeType: eebusutil.Ptr(limitScopeType), + }, + }, + } + feature.SetData(model.FunctionTypeLoadControlLimitDescriptionListData, desc) + + data = GetLocalLimitDescriptionsForTypeCategoryDirectionScope(s.service, limitType, limitCategory, limitDirection, limitScopeType) + assert.Equal(s.T(), 1, len(data)) + assert.NotNil(s.T(), data[0].LimitId) +} + +func (s *UtilSuite) Test_GetLocalLimitValueForLimitId() { + limitId := model.LoadControlLimitIdType(0) + + data := GetLocalLimitValueForLimitId(s.service, limitId) + assert.Nil(s.T(), data.LimitId) + + entity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + feature := entity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + + desc := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + }, + }, + } + feature.SetData(model.FunctionTypeLoadControlLimitListData, desc) + + data = GetLocalLimitValueForLimitId(s.service, limitId) + assert.NotNil(s.T(), data.LimitId) +} diff --git a/util/manufacturerdata.go b/util/manufacturerdata.go new file mode 100644 index 0000000..660dd80 --- /dev/null +++ b/util/manufacturerdata.go @@ -0,0 +1,46 @@ +package util + +import ( + "github.com/enbility/cemd/api" + eebusapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// return the current manufacturer data for a entity +// +// possible errors: +// - ErrNoCompatibleEntity if entity is not compatible +// - and others +func ManufacturerData(service eebusapi.ServiceInterface, entity spineapi.EntityRemoteInterface, entityTypes []model.EntityTypeType) (api.ManufacturerData, error) { + if entity == nil || !IsCompatibleEntity(entity, entityTypes) { + return api.ManufacturerData{}, api.ErrNoCompatibleEntity + } + + deviceClassification, err := DeviceClassification(service, entity) + if err != nil { + return api.ManufacturerData{}, err + } + + data, err := deviceClassification.GetManufacturerDetails() + if err != nil { + return api.ManufacturerData{}, err + } + + ret := api.ManufacturerData{ + DeviceName: Deref((*string)(data.DeviceName)), + DeviceCode: Deref((*string)(data.DeviceCode)), + SerialNumber: Deref((*string)(data.SerialNumber)), + SoftwareRevision: Deref((*string)(data.SoftwareRevision)), + HardwareRevision: Deref((*string)(data.HardwareRevision)), + VendorName: Deref((*string)(data.VendorName)), + VendorCode: Deref((*string)(data.VendorCode)), + BrandName: Deref((*string)(data.BrandName)), + PowerSource: Deref((*string)(data.PowerSource)), + ManufacturerNodeIdentification: Deref((*string)(data.ManufacturerNodeIdentification)), + ManufacturerLabel: Deref((*string)(data.ManufacturerLabel)), + ManufacturerDescription: Deref((*string)(data.ManufacturerDescription)), + } + + return ret, nil +} diff --git a/util/manufacturerdata_test.go b/util/manufacturerdata_test.go new file mode 100644 index 0000000..b5a2734 --- /dev/null +++ b/util/manufacturerdata_test.go @@ -0,0 +1,36 @@ +package util + +import ( + "github.com/enbility/ship-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UtilSuite) Test_ManufacturerData() { + entityTypes := []model.EntityTypeType{model.EntityTypeTypeEV} + + _, err := ManufacturerData(s.service, s.mockRemoteEntity, entityTypes) + assert.NotNil(s.T(), err) + + _, err = ManufacturerData(s.service, s.monitoredEntity, entityTypes) + assert.NotNil(s.T(), err) + + descData := &model.DeviceClassificationManufacturerDataType{ + + DeviceName: util.Ptr(model.DeviceClassificationStringType("deviceName")), + DeviceCode: util.Ptr(model.DeviceClassificationStringType("deviceCode")), + SerialNumber: util.Ptr(model.DeviceClassificationStringType("serialNumber")), + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeDeviceClassification, model.RoleTypeServer) + assert.NotNil(s.T(), rFeature) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceClassificationManufacturerData, descData, nil, nil) + assert.Nil(s.T(), fErr) + data, err := ManufacturerData(s.service, s.monitoredEntity, entityTypes) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), data) + assert.Equal(s.T(), "deviceName", data.DeviceName) + assert.Equal(s.T(), "deviceCode", data.DeviceCode) + assert.Equal(s.T(), "serialNumber", data.SerialNumber) + assert.Equal(s.T(), "", data.SoftwareRevision) +} diff --git a/util/measurement.go b/util/measurement.go new file mode 100644 index 0000000..eb0b40c --- /dev/null +++ b/util/measurement.go @@ -0,0 +1,110 @@ +package util + +import ( + "slices" + + eebusapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// Check the payload data if it contains measurementId values for a given scope +func MeasurementCheckPayloadDataForScope(service eebusapi.ServiceInterface, payload spineapi.EventPayload, scope model.ScopeTypeType) bool { + measurementF, err := Measurement(service, payload.Entity) + if err != nil || payload.Data == nil { + return false + } + + if data, err := measurementF.GetDescriptionsForScope(scope); err == nil { + measurements := payload.Data.(*model.MeasurementListDataType) + + for _, item := range data { + if item.MeasurementId == nil { + continue + } + + for _, measurement := range measurements.MeasurementData { + if measurement.MeasurementId != nil && + *measurement.MeasurementId == *item.MeasurementId && + measurement.Value != nil { + return true + } + } + } + } + + return false +} + +// return the phase specific voltage details +func MeasurementValuesForTypeCommodityScope( + service eebusapi.ServiceInterface, + entity spineapi.EntityRemoteInterface, + measurementType model.MeasurementTypeType, + commodityType model.CommodityTypeType, + scopeType model.ScopeTypeType, + energyDirection model.EnergyDirectionType, + validPhaseNameTypes []model.ElectricalConnectionPhaseNameType, +) ([]float64, error) { + measurement := measurementType + commodity := commodityType + scope := scopeType + data, err := GetValuesForTypeCommodityScope(service, entity, measurement, commodity, scope) + if err != nil { + return nil, err + } + + electricalConnection, err := ElectricalConnection(service, entity) + if err != nil || electricalConnection == nil { + return nil, err + } + + var result []float64 + + for _, item := range data { + if item.Value == nil || item.MeasurementId == nil { + continue + } + + if validPhaseNameTypes != nil { + param, err := electricalConnection.GetParameterDescriptionForMeasurementId(*item.MeasurementId) + if err != nil || + param.AcMeasuredPhases == nil || + !slices.Contains(validPhaseNameTypes, *param.AcMeasuredPhases) { + continue + } + } + + if energyDirection != "" { + desc, err := electricalConnection.GetDescriptionForMeasurementId(*item.MeasurementId) + if err != nil { + continue + } + + // if energy direction is not consume + if desc.PositiveEnergyDirection == nil || *desc.PositiveEnergyDirection != energyDirection { + return nil, err + } + } + + value := item.Value.GetValue() + + result = append(result, value) + } + + return result, nil +} + +func GetValuesForTypeCommodityScope( + service eebusapi.ServiceInterface, + entity spineapi.EntityRemoteInterface, + measurement model.MeasurementTypeType, + commodity model.CommodityTypeType, + scope model.ScopeTypeType) ([]model.MeasurementDataType, error) { + measurementFeature, err := Measurement(service, entity) + if err != nil || measurementFeature == nil { + return nil, err + } + + return measurementFeature.GetValuesForTypeCommodityScope(measurement, commodity, scope) +} diff --git a/util/measurement_test.go b/util/measurement_test.go new file mode 100644 index 0000000..4a96889 --- /dev/null +++ b/util/measurement_test.go @@ -0,0 +1,218 @@ +package util + +import ( + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UtilSuite) Test_MeasurementCheckPayloadDataForScope() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + + exists := MeasurementCheckPayloadDataForScope(s.service, payload, model.ScopeTypeTypeACPower) + assert.False(s.T(), exists) + + payload.Entity = s.monitoredEntity + + exists = MeasurementCheckPayloadDataForScope(s.service, payload, model.ScopeTypeTypeACPower) + assert.False(s.T(), exists) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACPower), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACPower), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + exists = MeasurementCheckPayloadDataForScope(s.service, payload, model.ScopeTypeTypeACPower) + assert.False(s.T(), exists) + + data := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + {}, + }, + } + payload.Data = data + + exists = MeasurementCheckPayloadDataForScope(s.service, payload, model.ScopeTypeTypeACPower) + assert.False(s.T(), exists) + + data = &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + Value: model.NewScaledNumberType(80), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(80), + }, + }, + } + + payload.Data = data + + exists = MeasurementCheckPayloadDataForScope(s.service, payload, model.ScopeTypeTypeACPower) + assert.True(s.T(), exists) +} + +func (s *UtilSuite) Test_MeasurementValuesForTypeCommodityScope() { + measurementType := model.MeasurementTypeTypePower + commodityType := model.CommodityTypeTypeElectricity + scopeType := model.ScopeTypeTypeACPower + energyDirection := model.EnergyDirectionTypeConsume + + data, err := MeasurementValuesForTypeCommodityScope( + s.service, + s.mockRemoteEntity, + measurementType, + commodityType, + scopeType, + energyDirection, + PhaseNameMapping, + ) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + data, err = MeasurementValuesForTypeCommodityScope( + s.service, + s.monitoredEntity, + measurementType, + commodityType, + scopeType, + energyDirection, + PhaseNameMapping, + ) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypePower), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACPower), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypePower), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACPower), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + MeasurementType: eebusutil.Ptr(model.MeasurementTypeTypePower), + CommodityType: eebusutil.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeACPower), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = MeasurementValuesForTypeCommodityScope( + s.service, + s.monitoredEntity, + measurementType, + commodityType, + scopeType, + energyDirection, + PhaseNameMapping, + ) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = MeasurementValuesForTypeCommodityScope( + s.service, + s.monitoredEntity, + measurementType, + commodityType, + scopeType, + energyDirection, + PhaseNameMapping, + ) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 0, len(data)) + + elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(1)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(2)), + AcMeasuredPhases: eebusutil.Ptr(model.ElectricalConnectionPhaseNameTypeC), + }, + }, + } + + rElFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, elParamData, nil, nil) + assert.Nil(s.T(), fErr) + + elDescData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + PositiveEnergyDirection: eebusutil.Ptr(model.EnergyDirectionTypeConsume), + }, + }, + } + + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionDescriptionListData, elDescData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = MeasurementValuesForTypeCommodityScope( + s.service, + s.monitoredEntity, + measurementType, + commodityType, + scopeType, + energyDirection, + PhaseNameMapping, + ) + assert.Nil(s.T(), err) + assert.Equal(s.T(), []float64{10, 10, 10}, data) +} diff --git a/util/testhelper_test.go b/util/testhelper_test.go new file mode 100644 index 0000000..53cd607 --- /dev/null +++ b/util/testhelper_test.go @@ -0,0 +1,227 @@ +package util + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/cemd/api" + eebusapi "github.com/enbility/eebus-go/api" + eebusmocks "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestUtilSuite(t *testing.T) { + suite.Run(t, new(UtilSuite)) +} + +type UtilSuite struct { + suite.Suite + + service eebusapi.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *mocks.EntityRemoteInterface + evseEntity spineapi.EntityRemoteInterface + monitoredEntity spineapi.EntityRemoteInterface +} + +func (s *UtilSuite) Event(ski string, entity spineapi.EntityRemoteInterface, event api.EventType) { +} + +func (s *UtilSuite) BeforeTest(suiteName, testName string) { + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := eebusapi.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := eebusmocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := mocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = mocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := mocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + + var entities []spineapi.EntityRemoteInterface + + s.remoteDevice, entities = setupDevices(s.service, s.T()) + s.evseEntity = entities[0] + s.monitoredEntity = entities[1] +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService eebusapi.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + []spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + localEntity := localDevice.EntityForType(model.EntityTypeTypeCEM) + + f := spine.NewFeatureLocal(1, localEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeClient) + localEntity.AddFeature(f) + f = spine.NewFeatureLocal(2, localEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeClient) + localEntity.AddFeature(f) + f = spine.NewFeatureLocal(3, localEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeClient) + localEntity.AddFeature(f) + f = spine.NewFeatureLocal(4, localEntity, model.FeatureTypeTypeDeviceClassification, model.RoleTypeClient) + localEntity.AddFeature(f) + f = spine.NewFeatureLocal(5, localEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeClient) + localEntity.AddFeature(f) + f = spine.NewFeatureLocal(1, localEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeLoadControlLimitDescriptionListData, true, false) + f.AddFunctionType(model.FunctionTypeLoadControlLimitListData, true, true) + localEntity.AddFeature(f) + f = spine.NewFeatureLocal(2, localEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeElectricalConnectionParameterDescriptionListData, true, false) + f.AddFunctionType(model.FunctionTypeElectricalConnectionPermittedValueSetListData, true, false) + f.AddFunctionType(model.FunctionTypeElectricalConnectionCharacteristicListData, true, true) + localEntity.AddFeature(f) + f = spine.NewFeatureLocal(3, localEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, true, false) + f.AddFunctionType(model.FunctionTypeDeviceConfigurationKeyValueListData, true, true) + localEntity.AddFeature(f) + f = spine.NewFeatureLocal(4, localEntity, model.FeatureTypeTypeDeviceClassification, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeDeviceClassificationManufacturerData, true, false) + f.AddFunctionType(model.FunctionTypeDeviceClassificationUserData, true, true) + localEntity.AddFeature(f) + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + role model.RoleType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeLoadControl, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeLoadControlLimitDescriptionListData, + model.FunctionTypeLoadControlLimitConstraintsListData, + model.FunctionTypeLoadControlLimitListData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + model.FunctionTypeElectricalConnectionPermittedValueSetListData, + }, + }, + {model.FeatureTypeTypeMeasurement, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeMeasurementDescriptionListData, + model.FunctionTypeMeasurementListData, + }, + }, + {model.FeatureTypeTypeDeviceClassification, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeDeviceClassificationManufacturerData, + model.FunctionTypeDeviceClassificationUserData, + }, + }, + {model.FeatureTypeTypeDeviceConfiguration, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, + model.FunctionTypeDeviceConfigurationKeyValueListData, + }, + }, + } + + remoteDeviceName := "remote" + + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: eebusutil.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + Feature: eebusutil.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: eebusutil.Ptr(feature.featureType), + Role: eebusutil.Ptr(feature.role), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeEVSE), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeEV), + }, + }, + }, + FeatureInformation: featureInformations, + } + + entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + + localDevice.AddRemoteDeviceForSki(remoteSki, remoteDevice) + + return remoteDevice, entities +}