Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions examples/remote/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"crypto/tls"
"flag"
"github.com/enbility/eebus-go/usecases/cem/ohpcf"
"log"
"net"
"os"
Expand Down Expand Up @@ -139,6 +140,13 @@ func main() {
log.Fatal(err)
}

err = r.RegisterUseCase(model.EntityTypeTypeCEM, "CEM-OHPCF", func(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) api.UseCaseInterface {
return ohpcf.NewOHPCF(localEntity, eventCB)
})
if err != nil {
log.Fatal(err)
}

ctx, cancelCtx := context.WithCancel(context.Background())
if err = r.Listen(ctx, "tcp", net.JoinHostPort("::", strconv.Itoa(3393))); err != nil {
log.Fatal(err)
Expand Down
85 changes: 85 additions & 0 deletions usecases/api/cem_ohpcf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package api

import (
"github.com/enbility/eebus-go/api"
spineapi "github.com/enbility/spine-go/api"
"github.com/enbility/spine-go/model"
"time"
)

type CemOHPCFInterface interface {
api.UseCaseInterface

// Scenario 1

// Get all relevant info for an available power consumption
//
// return OptionalPowerConsumptionInfo struct with all available info if an optional power consumption has been announced
OptionalPowerConsumption(entity spineapi.EntityRemoteInterface) (*OptionalPowerConsumptionInfo, error)

// The availability of an optional consumption of power [OHPCF-011/1].
//
// return true if the optional consumption of power is possible
OptionalPowerConsumptionAvailable(entity spineapi.EntityRemoteInterface) (bool, error)

// The requested power estimate value [OHPCF-011/2/1].
//
// return the requested power estimate value
RequestedPowerEstimate(entity spineapi.EntityRemoteInterface) (float64, error)

// The maximal value for the requested power estimate [OHPCF-011/2/1].
//
// return the maximal value for the requested power estimate
RequestedPowerMax(entity spineapi.EntityRemoteInterface) (float64, error)

// Indication whether the consumption may be stopped by the CEM [OHPCF-011/5].
//
// return true if the consumption may be stopped
ConsumptionIsStoppable(entity spineapi.EntityRemoteInterface) (bool, error)

// Indication whether the consumption may be paused and resumed by the CEM [OHPCF-011/6]
//
// return true if the consumption may be paused
ConsumptionIsPausable(entity spineapi.EntityRemoteInterface) (bool, error)

// The start time of the process [OHPCF-012/1].
//
// return the start time of the process
PowerConsumptionProcessStartTime(entity spineapi.EntityRemoteInterface) (time.Time, error)

// The current state of this power consumption process [OHPCF-012/2].
//
// return the current state of this power consumption process
PowerConsumptionProcessState(entity spineapi.EntityRemoteInterface) (CompressorPowerConsumptionStateType, error)

// The minimal time a consumption process must last [OHPCF-008].
//
// return the minimal time a consumption process must last
PowerConsumptionMinimalRunDuration(entity spineapi.EntityRemoteInterface) (time.Duration, error)

// The minimal time a pause of a consumption process must last [OHPCF-009].
//
// return the minimal time a pause of a consumption process must last
PowerConsumptionMinimalPauseDuration(entity spineapi.EntityRemoteInterface) (time.Duration, error)

// Scenario 2

// Schedule an optional power consumption process [OHPCF-004].
//
// note:
// Rescheduling an already scheduled power consumption process is possible as long as the
// scheduled process has not startet yet.
//
// parameters:
// - start: The start time of the power consumption
SchedulePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, start time.Time, resultCB func(result model.ResultDataType)) (*model.MsgCounterType, error)

@andig andig Jun 25, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hard-codes SchedulePowerConsumptionProcess to using model.NewAbsoluteOrRelativeTimeTypeFromTime(start) which doesn't work on Vaillant which only accepts durations. From the AbsoluteOrRelativeTimeType — EEBus_SPINE_TS_CommonDataTypes.xsd, lines 51–53:

<xs:simpleType name="AbsoluteOrRelativeTimeType">
    <xs:union memberTypes="xs:duration xs:dateTime"/>
</xs:simpleType>

I'd be happy to provide a PR. Options:

  • have both time and duration variants of the function
  • make start a variadic option (WithTime vs WithDuration)
  • simplify have the actual model type passed in (via interface or any)

Which one would you prefer?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems passing the start time as *model.AbsoluteOrRelativeTimeType is the absolute easiest way of doing this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took another look:

┌──────────────────────────────────────┬─────────────────────────────────┬───────────────────────────────┬────────────────────────────────────────────────────────┐
│               Use case               │              Field              │        Encoding chosen        │                      constructor                       │
├──────────────────────────────────────┼─────────────────────────────────┼───────────────────────────────┼────────────────────────────────────────────────────────┤
│ cs/lpc/public.go:81                  │ LoadControl limit EndTime       │ relative                      │ ...FromDuration(limit.Duration)                        │
├──────────────────────────────────────┼─────────────────────────────────┼───────────────────────────────┼────────────────────────────────────────────────────────┤
│ cs/lpp/public.go:83                  │ LoadControl limit EndTime       │ relative                      │ ...FromDuration(limit.Duration)                        │
├──────────────────────────────────────┼─────────────────────────────────┼───────────────────────────────┼────────────────────────────────────────────────────────┤
│ usecases/internal/loadcontrol.go:162 │ LoadControl limit EndTime       │ relative                      │ ...FromDuration(limit.Duration)                        │
├──────────────────────────────────────┼─────────────────────────────────┼───────────────────────────────┼────────────────────────────────────────────────────────┤
│ cem/cevc/public_scen2.go:111-130     │ TimeSeries Start/End            │ relative (incl. literal       │ ...FromDuration /                                      │
│                                      │                                 │ "PT0S")                       │ NewAbsoluteOrRelativeTimeType("PT0S")                  │
├──────────────────────────────────────┼─────────────────────────────────┼───────────────────────────────┼────────────────────────────────────────────────────────┤
│ cem/ohpcf/public.go:308              │ PowerSequence Schedule          │ absolute                      │ ...FromTime(start) ← outlier                           │
│                                      │ StartTime                       │                               │                                                        │
└──────────────────────────────────────┴─────────────────────────────────┴───────────────────────────────┴────────────────────────────────────────────────────────┘

Rest of the library is using duration. This is the only place where actual time seems to be used. So the idiomatic fix- without adding additional flexibility would be changing start into startIn as time.Duration.

wdyt?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should settle it:

3.1.8.2 Rules regarding the usage of time-related information
All time-related information within this Use Case SHALL be presented as relative times. This means
absolute times SHALL NOT be used. All start- and end-times SHALL be interpreted as relative to the
time the message was transmitted.


// stop (abort) the process [OHPCF-022/1].
AbortPowerConsumptionProcess(entity spineapi.EntityRemoteInterface, resultCB func(result model.ResultDataType)) (*model.MsgCounterType, error)

// pause the process [OHPCF-022/2].
PausePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, resultCB func(result model.ResultDataType)) (*model.MsgCounterType, error)

// resume the process [OHPCF-022/3].
ResumePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, resultCB func(result model.ResultDataType)) (*model.MsgCounterType, error)
}
21 changes: 21 additions & 0 deletions usecases/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@
EVChargeStateTypeFinished EVChargeStateType = "finished"
)

type OptionalPowerConsumptionInfo struct {
PowerSequenceId model.PowerSequenceIdType
Power *float64

Check failure on line 22 in usecases/api/types.go

View workflow job for this annotation

GitHub Actions / Build

File is not properly formatted (gofmt)
MaxPower *float64
State model.PowerSequenceStateType
IsPausable bool
IsStoppable bool
StartTime *time.Time
}

// manufacturer data type
type ManufacturerData struct {
DeviceName string
Expand Down Expand Up @@ -167,6 +177,17 @@
Value float64 // Energy Cost or Power Limit
}

type CompressorPowerConsumptionStateType string

const (
CompressorPowerConsumptionStateAvailable CompressorPowerConsumptionStateType = "available"
CompressorPowerConsumptionStateScheduled CompressorPowerConsumptionStateType = "scheduled"
CompressorPowerConsumptionStateRunning CompressorPowerConsumptionStateType = "running"
CompressorPowerConsumptionStatePaused CompressorPowerConsumptionStateType = "paused"
CompressorPowerConsumptionStateCompleted CompressorPowerConsumptionStateType = "completed"
CompressorPowerConsumptionStateStopped CompressorPowerConsumptionStateType = "stopped" // (aborted)
)

type PendingDeviceConfiguration struct {
Description model.DeviceConfigurationKeyValueDescriptionDataType `json:"description"`
KeyName model.DeviceConfigurationKeyNameType `json:"keyName"`
Expand Down
121 changes: 121 additions & 0 deletions usecases/cem/ohpcf/events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package ohpcf

import (
"github.com/enbility/eebus-go/features/client"
"github.com/enbility/eebus-go/usecases/internal"
"github.com/enbility/ship-go/logging"
spineapi "github.com/enbility/spine-go/api"
"github.com/enbility/spine-go/model"
)

// handle SPINE events
func (o *OHPCF) HandleEvent(payload spineapi.EventPayload) {
// only about events from an EV entity or device changes for this remote device

if !o.IsCompatibleEntityType(payload.Entity) {
return
}

if internal.IsEntityAdded(payload) {
o.connected(payload.Entity)
}

if payload.Data == nil {
return
}

switch payload.Data.(type) {
case *model.SmartEnergyManagementPsDataType:
o.loadSmartEnergyManagementPsDataType(payload)
}
}

func (o *OHPCF) connected(entity spineapi.EntityRemoteInterface) {
if semp, err := client.NewSmartEnergyManagementPs(o.LocalEntity, entity); err == nil {
if !semp.HasSubscription() {
if _, err := semp.Subscribe(); err != nil {
logging.Log().Debug(err)
}
}

if !semp.HasBinding() {
if _, err := semp.Bind(); err != nil {
logging.Log().Debug(err)
}
}
}
}

func (o *OHPCF) loadSmartEnergyManagementPsDataType(payload spineapi.EventPayload) {
data := payload.Data.(*model.SmartEnergyManagementPsDataType)

if o.EventCB == nil {
return
}

if len(data.Alternatives) == 1 {
alternative := data.Alternatives[0]

if len(alternative.PowerSequence) != 1 {
return
}

request := alternative.PowerSequence[0]

if len(request.PowerTimeSlot) == 1 &&
request.PowerTimeSlot[0].ValueList != nil &&
len(request.PowerTimeSlot[0].ValueList.Value) > 0 {
for _, value := range request.PowerTimeSlot[0].ValueList.Value {
if value.Value != nil &&
value.ValueType != nil {
if *value.ValueType == model.PowerTimeSlotValueTypeTypePower {
o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateRequestedPowerEstimate)
} else if *value.ValueType == model.PowerTimeSlotValueTypeTypePowerMax {
o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateRequestedPowerMax)
}
}
}
}

if request.OperatingConstraintsInterrupt != nil {
if request.OperatingConstraintsInterrupt.IsStoppable != nil {
// [OHPCF-011/5]
// [OHPCF-012/3]
o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionIsStoppable)
}

if request.OperatingConstraintsInterrupt.IsPausable != nil {
// [OHPCF-011/6]
// [OHPCF-012/3]
o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionIsPausable)
}
}

if request.Schedule != nil &&
request.Schedule.StartTime != nil {
// [OHPCF-012/1]
o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionStartTime)
}

if request.State != nil &&
request.State.State != nil {
// [OHPCF-006], [OHPCF-011/1], [OHPCF-012/2], [OHPCF-022]
o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionState)
}

if request.OperatingConstraintsDuration != nil {
if request.OperatingConstraintsDuration.ActiveDurationMin != nil {
// [OHPCF-008]
o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateMinimalRunDuration)
}
if request.OperatingConstraintsDuration.PauseDurationMin != nil {
// [OHPCF-009]
o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateMinimalPauseDuration)
}
}

} else if len(data.Alternatives) == 0 {

Check failure on line 117 in usecases/cem/ohpcf/events.go

View workflow job for this annotation

GitHub Actions / Build

unnecessary trailing newline (whitespace)
// [OHPCF-003], [OHPCF-006/2]
o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionState)
}
}
Loading
Loading