EEBus: add OHPCF heat pump compressor flexibility (#30636)

This commit is contained in:
andig 2026-06-29 11:54:53 +02:00 • committed by GitHub
parent cdb2914308
commit 1005f075a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 656 additions and 7 deletions

View file

@ -9,7 +9,7 @@ import (
"golang.org/x/oauth2"
)
//go:generate go tool mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,CurrentGetter,PhaseSwitcher,PhaseGetter,FeatureDescriber,Identifier,Meter,MeterEnergy,MeterReturnEnergy,PhaseCurrents,Vehicle,ConnectionTimer,ChargeRater,Battery,BatteryController,BatterySocLimiter,Circuit,Dimmer,HEMS,Tariff
//go:generate go tool mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,PowerLimiter,CurrentGetter,PhaseSwitcher,PhaseGetter,FeatureDescriber,Identifier,Meter,MeterEnergy,MeterReturnEnergy,PhaseCurrents,Vehicle,ConnectionTimer,ChargeRater,Battery,BatteryController,BatterySocLimiter,Circuit,Dimmer,HEMS,Tariff
// Meter provides total active power in W
type Meter interface {
@ -192,6 +192,11 @@ type CurrentLimiter interface {
GetMinMaxCurrent() (float64, float64, error)
}
// PowerLimiter returns the power limits in W
type PowerLimiter interface {
GetMinMaxPower() (float64, float64, error)
}
// SocLimiter returns the soc limit
type SocLimiter interface {
GetLimitSoc() (int64, error)

View file

@ -1,9 +1,9 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,CurrentLimiter,CurrentGetter,PhaseSwitcher,PhaseGetter,FeatureDescriber,Identifier,Meter,MeterEnergy,MeterReturnEnergy,PhaseCurrents,Vehicle,ConnectionTimer,ChargeRater,Battery,BatteryController,BatterySocLimiter,Circuit,Dimmer,HEMS,Tariff)
// Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,CurrentLimiter,PowerLimiter,CurrentGetter,PhaseSwitcher,PhaseGetter,FeatureDescriber,Identifier,Meter,MeterEnergy,MeterReturnEnergy,PhaseCurrents,Vehicle,ConnectionTimer,ChargeRater,Battery,BatteryController,BatterySocLimiter,Circuit,Dimmer,HEMS,Tariff)
//
// Generated by this command:
//
// mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,CurrentGetter,PhaseSwitcher,PhaseGetter,FeatureDescriber,Identifier,Meter,MeterEnergy,MeterReturnEnergy,PhaseCurrents,Vehicle,ConnectionTimer,ChargeRater,Battery,BatteryController,BatterySocLimiter,Circuit,Dimmer,HEMS,Tariff
// mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,PowerLimiter,CurrentGetter,PhaseSwitcher,PhaseGetter,FeatureDescriber,Identifier,Meter,MeterEnergy,MeterReturnEnergy,PhaseCurrents,Vehicle,ConnectionTimer,ChargeRater,Battery,BatteryController,BatterySocLimiter,Circuit,Dimmer,HEMS,Tariff
//
// Package api is a generated GoMock package.
@ -177,6 +177,46 @@ func (mr *MockCurrentLimiterMockRecorder) GetMinMaxCurrent() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMinMaxCurrent", reflect.TypeOf((*MockCurrentLimiter)(nil).GetMinMaxCurrent))
}
// MockPowerLimiter is a mock of PowerLimiter interface.
type MockPowerLimiter struct {
ctrl *gomock.Controller
recorder *MockPowerLimiterMockRecorder
isgomock struct{}
}
// MockPowerLimiterMockRecorder is the mock recorder for MockPowerLimiter.
type MockPowerLimiterMockRecorder struct {
mock *MockPowerLimiter
}
// NewMockPowerLimiter creates a new mock instance.
func NewMockPowerLimiter(ctrl *gomock.Controller) *MockPowerLimiter {
mock := &MockPowerLimiter{ctrl: ctrl}
mock.recorder = &MockPowerLimiterMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPowerLimiter) EXPECT() *MockPowerLimiterMockRecorder {
return m.recorder
}
// GetMinMaxPower mocks base method.
func (m *MockPowerLimiter) GetMinMaxPower() (float64, float64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetMinMaxPower")
ret0, _ := ret[0].(float64)
ret1, _ := ret[1].(float64)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// GetMinMaxPower indicates an expected call of GetMinMaxPower.
func (mr *MockPowerLimiterMockRecorder) GetMinMaxPower() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMinMaxPower", reflect.TypeOf((*MockPowerLimiter)(nil).GetMinMaxPower))
}
// MockCurrentGetter is a mock of CurrentGetter interface.
type MockCurrentGetter struct {
ctrl *gomock.Controller

457
charger/eebus-ohpcf.go Normal file
View file

@ -0,0 +1,457 @@
package charger
import (
"context"
"errors"
"fmt"
"sync"
"time"
eebusapi "github.com/enbility/eebus-go/api"
ucapi "github.com/enbility/eebus-go/usecases/api"
"github.com/enbility/eebus-go/usecases/cem/ohpcf"
"github.com/enbility/eebus-go/usecases/ma/mdt"
"github.com/enbility/eebus-go/usecases/ma/mpc"
spineapi "github.com/enbility/spine-go/api"
"github.com/enbility/spine-go/model"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/server/eebus"
"github.com/evcc-io/evcc/util"
)
// EEBusOHPCF controls a remote heat pump compressor via the EEBus OHPCF use case
// (Optimization of Self-Consumption by Heat Pump Compressor Flexibility).
//
// The compressor announces an optional power consumption that the CEM may
// schedule, pause, resume or abort. evcc models this as an on/off switch:
// enabling the charger schedules or resumes the optional consumption, disabling
// it pauses or aborts the running process.
type EEBusOHPCF struct {
*embed
cem *eebus.CustomerEnergyManagement
ma *eebus.MonitoringAppliance
ctx context.Context
reboost time.Duration
mu sync.RWMutex
log *util.Logger
compressor spineapi.EntityRemoteInterface
mpcEntity spineapi.EntityRemoteInterface
dhwEntity spineapi.EntityRemoteInterface
enabled bool
reboosting bool
connector *eebus.Connector
}
// errNotConnected is returned whenever the compressor entity is not (yet) available.
var errNotConnected = errors.New("not connected")
func init() {
registry.AddCtx("eebus-ohpcf", NewEEBusOHPCFFromConfig)
}
// NewEEBusOHPCFFromConfig creates an EEBus OHPCF charger from generic config
func NewEEBusOHPCFFromConfig(ctx context.Context, other map[string]any) (api.Charger, error) {
cc := struct {
embed `mapstructure:",squash"`
Ski string
Ip string
Reboost time.Duration
}{
embed: embed{
Features_: []api.Feature{api.Heating, api.IntegratedDevice},
},
Reboost: 10 * time.Minute,
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
return NewEEBusOHPCF(ctx, &cc.embed, cc.Ski, cc.Ip, cc.Reboost)
}
// NewEEBusOHPCF creates an EEBus OHPCF charger, registers it with the EEBus
// instance and waits for the connection.
func NewEEBusOHPCF(ctx context.Context, embed *embed, ski, ip string, reboost time.Duration) (api.Charger, error) {
inst, err := eebus.Instance()
if err != nil {
return nil, err
}
c := &EEBusOHPCF{
embed: embed,
log: util.NewLogger("eebus-ohpcf"),
cem: inst.CustomerEnergyManagement(),
ma: inst.MonitoringAppliance(),
connector: eebus.NewConnector(),
ctx: ctx,
reboost: reboost,
}
if err := inst.RegisterDevice(ski, ip, c); err != nil {
return nil, err
}
if err := c.connector.Wait(ctx); err != nil {
inst.UnregisterDevice(ski, c)
return nil, err
}
// unregister device when context is cancelled (e.g. UI config validation)
go func() {
<-ctx.Done()
inst.UnregisterDevice(ski, c)
}()
return c, nil
}
var _ eebus.Device = (*EEBusOHPCF)(nil)
// Connect implements the eebus.Device interface
func (c *EEBusOHPCF) Connect(connected bool) {
c.connector.Connect(connected)
if connected {
return
}
c.mu.Lock()
defer c.mu.Unlock()
c.compressor = nil
c.mpcEntity = nil
c.dhwEntity = nil
}
// UseCaseEvent implements the eebus.Device interface
func (c *EEBusOHPCF) UseCaseEvent(_ spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event eebusapi.EventType) {
// device/entity removal fires the use case update event with a nil entity
if entity == nil {
return
}
switch event {
case ohpcf.UseCaseSupportUpdate,
ohpcf.DataUpdateRequestedPowerEstimate,
ohpcf.DataUpdateRequestedPowerMax,
ohpcf.DataUpdateConsumptionIsStoppable,
ohpcf.DataUpdateConsumptionIsPausable,
ohpcf.DataUpdateConsumptionStartTime,
ohpcf.DataUpdateConsumptionState,
ohpcf.DataUpdateMinimalRunDuration,
ohpcf.DataUpdateMinimalPauseDuration:
c.mu.Lock()
c.compressor = entity
c.mu.Unlock()
// Monitoring Appliance MPC provides the measured power consumption
case mpc.UseCaseSupportUpdate:
c.mu.Lock()
// use most specific selector
if c.mpcEntity == nil || len(entity.Address().Entity) < len(c.mpcEntity.Address().Entity) {
c.mpcEntity = entity
}
c.mu.Unlock()
// Monitoring Appliance MDT provides the DHW temperature
case mdt.UseCaseSupportUpdate, mdt.DataUpdateTemperature:
c.mu.Lock()
c.dhwEntity = entity
c.mu.Unlock()
}
}
func (c *EEBusOHPCF) connectedCompressor() (spineapi.EntityRemoteInterface, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
return c.compressor, c.compressor != nil
}
func (c *EEBusOHPCF) setEnabled(enabled bool) {
c.mu.Lock()
defer c.mu.Unlock()
c.enabled = enabled
}
func (c *EEBusOHPCF) lastEnabled() bool {
c.mu.RLock()
defer c.mu.RUnlock()
return c.enabled
}
// ohpcfStatus maps the compressor process state to a charge status: running is
// consuming (C), any other connected state is standby (B). Disconnected (A) is handled in Status.
func ohpcfStatus(state ucapi.CompressorPowerConsumptionStateType) api.ChargeStatus {
if state == ucapi.CompressorPowerConsumptionStateRunning {
return api.StatusC
}
return api.StatusB
}
var _ api.Charger = (*EEBusOHPCF)(nil)
// Status implements the api.Charger interface
func (c *EEBusOHPCF) Status() (api.ChargeStatus, error) {
entity, ok := c.connectedCompressor()
if !ok {
return api.StatusNone, errNotConnected
}
state, err := c.cem.OHPCF.PowerConsumptionProcessState(entity)
if err != nil {
// connected but no flexibility announced yet: standby, not disconnected
return api.StatusB, nil
}
return ohpcfStatus(state), nil
}
// Enabled reports the commanded on/off intent; Status reflects the actual
// compressor state.
func (c *EEBusOHPCF) Enabled() (bool, error) {
if _, ok := c.connectedCompressor(); !ok {
return false, errNotConnected
}
return c.lastEnabled(), nil
}
// Enable schedules/resumes the optional consumption when on, pauses/aborts it
// when off; while on a reboost loop reschedules newly announced consumption.
func (c *EEBusOHPCF) Enable(enable bool) error {
c.setEnabled(enable)
if enable {
c.startReboost()
}
return c.apply()
}
// startReboost launches the reboost loop, unless one is already running or no
// reboost interval is configured.
func (c *EEBusOHPCF) startReboost() {
if c.reboost <= 0 {
return
}
c.mu.Lock()
defer c.mu.Unlock()
if c.reboosting {
return
}
c.reboosting = true
go c.reboostLoop()
}
// reboostLoop reschedules a freshly announced optional consumption after each
// reboost interval; it exits when the charger is disabled or the context ends.
func (c *EEBusOHPCF) reboostLoop() {
defer func() {
c.mu.Lock()
c.reboosting = false
c.mu.Unlock()
}()
for {
select {
case <-c.ctx.Done():
return
case <-time.After(c.reboost):
if !c.lastEnabled() {
return
}
if err := c.apply(); err != nil {
c.log.DEBUG.Printf("reboost: %v", err)
}
}
}
}
type ohpcfAction int
const (
ohpcfNone ohpcfAction = iota
ohpcfSchedule
ohpcfResume
ohpcfStop
)
// ohpcfControlAction returns the command needed to reach the desired on/off
// state; it returns an action only on a state transition, so repeats are no-ops.
func ohpcfControlAction(state ucapi.CompressorPowerConsumptionStateType, enable bool) ohpcfAction {
if enable {
switch state {
case ucapi.CompressorPowerConsumptionStateAvailable:
return ohpcfSchedule
case ucapi.CompressorPowerConsumptionStatePaused:
return ohpcfResume
}
return ohpcfNone
}
switch state {
case ucapi.CompressorPowerConsumptionStateRunning,
ucapi.CompressorPowerConsumptionStateScheduled:
return ohpcfStop
}
return ohpcfNone
}
// stop pauses the optional consumption if the compressor permits it, otherwise
// it aborts the process.
func (c *EEBusOHPCF) stop(entity spineapi.EntityRemoteInterface) error {
if pausable, err := c.cem.OHPCF.ConsumptionIsPausable(entity); err == nil && pausable {
return c.await(func(cb func(model.ResultDataType)) (*model.MsgCounterType, error) {
return c.cem.OHPCF.PausePowerConsumptionProcess(entity, cb)
})
}
if stoppable, err := c.cem.OHPCF.ConsumptionIsStoppable(entity); err == nil && stoppable {
return c.await(func(cb func(model.ResultDataType)) (*model.MsgCounterType, error) {
return c.cem.OHPCF.AbortPowerConsumptionProcess(entity, cb)
})
}
return api.ErrNotAvailable
}
// ohpcfWriteTimeout bounds how long a control write waits for its result.
const ohpcfWriteTimeout = 10 * time.Second
// await runs a control write and waits for the heat pump's result, returning an
// error if the write is rejected or no result arrives within the timeout.
func (c *EEBusOHPCF) await(write func(func(model.ResultDataType)) (*model.MsgCounterType, error)) error {
res := make(chan model.ResultDataType, 1)
if _, err := write(func(r model.ResultDataType) { res <- r }); err != nil {
return err
}
select {
case r := <-res:
if r.ErrorNumber != nil && *r.ErrorNumber != 0 {
err := fmt.Errorf("write rejected: %d", *r.ErrorNumber)
if r.Description != nil {
err = fmt.Errorf("%w (%s)", err, *r.Description)
}
c.log.ERROR.Println(err)
return err
}
return nil
case <-time.After(ohpcfWriteTimeout):
return errors.New("write result timeout")
}
}
// MaxCurrent implements the api.Charger interface. OHPCF is on/off and cannot
// be modulated, so the offered current is ignored.
func (c *EEBusOHPCF) MaxCurrent(int64) error {
return c.apply()
}
// apply issues the command to align the optional consumption with the on/off
// intent. It is idempotent: ohpcfControlAction only acts on a state transition.
func (c *EEBusOHPCF) apply() error {
entity, ok := c.connectedCompressor()
if !ok {
return errNotConnected
}
state, err := c.cem.OHPCF.PowerConsumptionProcessState(entity)
if err != nil {
// no process state announced yet, nothing to control
return nil
}
switch ohpcfControlAction(state, c.lastEnabled()) {
case ohpcfSchedule:
return c.await(func(cb func(model.ResultDataType)) (*model.MsgCounterType, error) {
// 0 = start immediately (relative schedule, see SchedulePowerConsumptionProcess)
return c.cem.OHPCF.SchedulePowerConsumptionProcess(entity, 0, cb)
})
case ohpcfResume:
return c.await(func(cb func(model.ResultDataType)) (*model.MsgCounterType, error) {
return c.cem.OHPCF.ResumePowerConsumptionProcess(entity, cb)
})
case ohpcfStop:
return c.stop(entity)
}
return nil
}
var _ api.PowerLimiter = (*EEBusOHPCF)(nil)
// GetMinMaxPower implements the api.PowerLimiter interface, reporting the
// announced optional consumption (estimate..max) or ErrNotAvailable if none.
func (c *EEBusOHPCF) GetMinMaxPower() (float64, float64, error) {
entity, ok := c.connectedCompressor()
if !ok {
return 0, 0, errNotConnected
}
estimate, err := c.cem.OHPCF.RequestedPowerEstimate(entity)
if err != nil {
return 0, 0, api.ErrNotAvailable
}
maxPower, err := c.cem.OHPCF.RequestedPowerMax(entity)
if err != nil {
maxPower = estimate
}
return estimate, maxPower, nil
}
var _ api.Meter = (*EEBusOHPCF)(nil)
// CurrentPower implements the api.Meter interface and reports the heat pump's
// measured power consumption via the MPC use case.
func (c *EEBusOHPCF) CurrentPower() (float64, error) {
c.mu.RLock()
entity := c.mpcEntity
c.mu.RUnlock()
if entity == nil || !c.ma.MaMPCInterface.IsScenarioAvailableAtEntity(entity, eebus.MPCPower) {
return 0, api.ErrNotAvailable
}
power, err := c.ma.MaMPCInterface.Power(entity)
if err != nil {
return 0, eebus.WrapError(err)
}
return power, nil
}
var _ api.Battery = (*EEBusOHPCF)(nil)
// Soc implements the api.Battery interface and reports the heat pump's domestic
// hot water temperature in °C via the MDT use case.
func (c *EEBusOHPCF) Soc() (float64, error) {
c.mu.RLock()
entity := c.dhwEntity
c.mu.RUnlock()
if entity == nil || !c.ma.MaMDTInterface.IsScenarioAvailableAtEntity(entity, eebus.MDTTemperature) {
return 0, api.ErrNotAvailable
}
temp, err := c.ma.MaMDTInterface.Temperature(entity, model.UnitOfMeasurementTypedegC)
if err != nil {
return 0, eebus.WrapError(err)
}
return temp, nil
}

View file

@ -0,0 +1,69 @@
package charger
import (
"testing"
ucapi "github.com/enbility/eebus-go/usecases/api"
"github.com/evcc-io/evcc/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// methods accessing the compressor entity must error when it is absent,
// so a missing compressor is not mistaken for an idle device.
func TestEEBusOHPCFNotConnected(t *testing.T) {
c := &EEBusOHPCF{}
status, err := c.Status()
require.ErrorIs(t, err, errNotConnected)
assert.Equal(t, api.StatusNone, status)
_, err = c.Enabled()
require.ErrorIs(t, err, errNotConnected)
require.ErrorIs(t, c.Enable(true), errNotConnected)
require.ErrorIs(t, c.MaxCurrent(16), errNotConnected)
}
// status mapping: running is C, every other connected state (incl. completed
// and stopped after a boost) is standby B, never disconnected.
func TestOHPCFStatus(t *testing.T) {
tc := []struct {
state ucapi.CompressorPowerConsumptionStateType
want api.ChargeStatus
}{
{ucapi.CompressorPowerConsumptionStateRunning, api.StatusC},
{ucapi.CompressorPowerConsumptionStateAvailable, api.StatusB},
{ucapi.CompressorPowerConsumptionStateScheduled, api.StatusB},
{ucapi.CompressorPowerConsumptionStatePaused, api.StatusB},
{ucapi.CompressorPowerConsumptionStateCompleted, api.StatusB},
{ucapi.CompressorPowerConsumptionStateStopped, api.StatusB},
}
for _, tc := range tc {
assert.Equal(t, tc.want, ohpcfStatus(tc.state), "%v", tc.state)
}
}
// on/off control: enable schedules/resumes, disable stops, and an already
// running/scheduled process issues no further command.
func TestOHPCFControlAction(t *testing.T) {
tc := []struct {
state ucapi.CompressorPowerConsumptionStateType
enable bool
want ohpcfAction
}{
{ucapi.CompressorPowerConsumptionStateAvailable, true, ohpcfSchedule},
{ucapi.CompressorPowerConsumptionStatePaused, true, ohpcfResume},
{ucapi.CompressorPowerConsumptionStateScheduled, true, ohpcfNone},
{ucapi.CompressorPowerConsumptionStateRunning, true, ohpcfNone},
{ucapi.CompressorPowerConsumptionStateRunning, false, ohpcfStop},
{ucapi.CompressorPowerConsumptionStateScheduled, false, ohpcfStop},
{ucapi.CompressorPowerConsumptionStateAvailable, false, ohpcfNone},
{ucapi.CompressorPowerConsumptionStatePaused, false, ohpcfNone},
}
for _, tc := range tc {
assert.Equal(t, tc.want, ohpcfControlAction(tc.state, tc.enable), "%v enable=%v", tc.state, tc.enable)
}
}

View file

@ -161,6 +161,14 @@ func (lp *Loadpoint) effectiveMinCurrent() float64 {
}
}
// power-limited chargers (e.g. EEBus OHPCF heat pump) report their demand in
// W; convert to per-phase current so the PV enable gate covers it
if c, ok := api.Cap[api.PowerLimiter](lp.charger); ok {
if res, _, err := c.GetMinMaxPower(); err == nil && res > 0 {
chargerMin = res / (Voltage * float64(lp.minActivePhases()))
}
}
switch {
case max(vehicleMin, chargerMin) == 0:
return lpMin
@ -187,6 +195,12 @@ func (lp *Loadpoint) effectiveMaxCurrent() float64 {
}
}
if c, ok := api.Cap[api.PowerLimiter](lp.charger); ok {
if _, res, err := c.GetMinMaxPower(); err == nil && res > 0 {
maxCurrent = min(maxCurrent, res/(Voltage*float64(lp.maxActivePhases())))
}
}
return maxCurrent
}

View file

@ -72,6 +72,29 @@ func TestEffectiveMinMaxCurrent(t *testing.T) {
}
}
func TestEffectivePowerLimiter(t *testing.T) {
Voltage = 230
ctrl := gomock.NewController(t)
lp := NewLoadpoint(util.NewLogger("foo"), nil)
phases := float64(lp.minActivePhases()) // == maxActivePhases for default lp
powerLimiter := api.NewMockPowerLimiter(ctrl)
// min 10A, max 12A worth of power across all phases
powerLimiter.EXPECT().GetMinMaxPower().Return(230*phases*10, 230*phases*12, nil).AnyTimes()
lp.charger = struct {
api.Charger
api.PowerLimiter
}{
Charger: api.NewMockCharger(ctrl),
PowerLimiter: powerLimiter,
}
assert.Equal(t, 10.0, lp.effectiveMinCurrent(), "min")
assert.Equal(t, 12.0, lp.effectiveMaxCurrent(), "max")
}
func TestNextPlan(t *testing.T) {
clock := clock.NewMock()

4
go.mod
View file

@ -29,7 +29,7 @@ require (
github.com/denisbrodbeck/machineid v1.0.1
github.com/dylanmei/iso8601 v0.1.0
github.com/eclipse/paho.mqtt.golang v1.5.1
github.com/enbility/eebus-go v0.7.1-0.20260520155652-11bc53084557
github.com/enbility/eebus-go v0.7.1-0.20260608105936-abdcf864d0cf
github.com/enbility/ship-go v0.6.1-0.20260518113001-134687068e3c
github.com/enbility/spine-go v0.7.1-0.20260520153416-0104ce40c885
github.com/evcc-io/openapi-mcp v0.6.1-0.20260503092507-6199c7ad3baf
@ -261,3 +261,5 @@ tool (
replace github.com/grid-x/modbus => github.com/evcc-io/modbus v0.0.0-20250501165638-8b6f1fbdb7ea
replace github.com/lorenzodonini/ocpp-go => github.com/evcc-io/ocpp-go v0.0.0-20251212212612-b7f92ee0443b
replace github.com/enbility/eebus-go => github.com/evcc-io/eebus-go v0.0.0-20260627085352-933afd2a4ea6

4
go.sum
View file

@ -136,8 +136,6 @@ github.com/eclipse/paho.golang v0.23.0 h1:KHgl2wz6EJo7cMBmkuhpt7C576vP+kpPv7jjvS
github.com/eclipse/paho.golang v0.23.0/go.mod h1:nQRhTkoZv8EAiNs5UU0/WdQIx2NrnWUpL9nsGJTQN04=
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
github.com/enbility/eebus-go v0.7.1-0.20260520155652-11bc53084557 h1:YQvO8wiUG1OEjFAkKtLTrCFze8KpG7YZ1TwrQKj9H54=
github.com/enbility/eebus-go v0.7.1-0.20260520155652-11bc53084557/go.mod h1:L1nBEy/dBASQRoz/9JF7KEmqFdnXHLbZqDpxiA0ezr4=
github.com/enbility/go-avahi v0.0.0-20240909195612-d5de6b280d7a h1:foChWb8lhzqa6lWDRs6COYMdp649YlUirFP8GqoT0JQ=
github.com/enbility/go-avahi v0.0.0-20240909195612-d5de6b280d7a/go.mod h1:H64mhYcAQUGUUnVqMdZQf93kPecH4M79xwH95Lddt3U=
github.com/enbility/ship-go v0.6.1-0.20260518113001-134687068e3c h1:ZNYyr/SDaAMHi6ubJqWuoId8QUu5V4xXWCIRf7VnfCY=
@ -146,6 +144,8 @@ github.com/enbility/spine-go v0.7.1-0.20260520153416-0104ce40c885 h1:nBqjAPONpI/
github.com/enbility/spine-go v0.7.1-0.20260520153416-0104ce40c885/go.mod h1:ddWZU5BQGyzRGF20Z7rUKFFzbCp+cfu9mZgYtOMW/fM=
github.com/enbility/zeroconf/v2 v2.0.0-20240920094356-be1cae74fda6 h1:XOYvxKtT1oxT37w/5oEiRLuPbm9FuJPt3fiYhX0h8Po=
github.com/enbility/zeroconf/v2 v2.0.0-20240920094356-be1cae74fda6/go.mod h1:BszP9qFV14mPXgyIREbgIdQtWxbAj3OKqvK02HihMoM=
github.com/evcc-io/eebus-go v0.0.0-20260627085352-933afd2a4ea6 h1:ax+dfxmeF/hN3IIUXFnDw3y3BE7pr0m/699k08+Q9sM=
github.com/evcc-io/eebus-go v0.0.0-20260627085352-933afd2a4ea6/go.mod h1:L1nBEy/dBASQRoz/9JF7KEmqFdnXHLbZqDpxiA0ezr4=
github.com/evcc-io/modbus v0.0.0-20250501165638-8b6f1fbdb7ea h1:F6eyC8V8wvc3ranlsR4coAls+OPBkVvxyX0a2VqdlPc=
github.com/evcc-io/modbus v0.0.0-20250501165638-8b6f1fbdb7ea/go.mod h1:swrNGAVgI1r/3d/sEKE7qgujdRR9aHVPYKyc3gvpVTc=
github.com/evcc-io/ocpp-go v0.0.0-20251212212612-b7f92ee0443b h1:5vFr7wOwoi5ylybUnxxBk1U3FRF2+3Sf0sKx8thxw18=

View file

@ -17,12 +17,14 @@ import (
"github.com/enbility/eebus-go/usecases/cem/evcem"
"github.com/enbility/eebus-go/usecases/cem/evsecc"
"github.com/enbility/eebus-go/usecases/cem/evsoc"
"github.com/enbility/eebus-go/usecases/cem/ohpcf"
"github.com/enbility/eebus-go/usecases/cem/opev"
"github.com/enbility/eebus-go/usecases/cem/oscev"
csplc "github.com/enbility/eebus-go/usecases/cs/lpc"
cslpp "github.com/enbility/eebus-go/usecases/cs/lpp"
eglpc "github.com/enbility/eebus-go/usecases/eg/lpc"
eglpp "github.com/enbility/eebus-go/usecases/eg/lpp"
"github.com/enbility/eebus-go/usecases/ma/mdt"
"github.com/enbility/eebus-go/usecases/ma/mgcp"
"github.com/enbility/eebus-go/usecases/ma/mpc"
shipapi "github.com/enbility/ship-go/api"
@ -47,6 +49,7 @@ type CustomerEnergyManagement struct {
EvSoc ucapi.CemEVSOCInterface
OpEV ucapi.CemOPEVInterface
OscEV ucapi.CemOSCEVInterface
OHPCF ucapi.CemOHPCFInterface
}
// Controllable System
@ -59,6 +62,7 @@ type ControllableSystem struct {
type MonitoringAppliance struct {
ucapi.MaMGCPInterface
ucapi.MaMPCInterface
ucapi.MaMDTInterface
}
// Energy Guard
@ -200,12 +204,14 @@ func NewServer(other Config) (*EEBus, error) {
OpEV: opev.NewOPEV(localEntity, c.ucCallback),
OscEV: oscev.NewOSCEV(localEntity, c.ucCallback),
EvSoc: evsoc.NewEVSOC(localEntity, c.ucCallback),
OHPCF: ohpcf.NewOHPCF(localEntity, c.ucCallback),
}
// monitoring appliance to meters
c.ma = MonitoringAppliance{
MaMGCPInterface: mgcp.NewMGCP(localEntity, c.ucCallback),
MaMPCInterface: mpc.NewMPC(localEntity, c.ucCallback),
MaMDTInterface: mdt.NewMDT(localEntity, c.ucCallback),
}
}
@ -240,8 +246,9 @@ func NewServer(other Config) (*EEBus, error) {
c.cem.EvseCC, c.cem.EvCC,
c.cem.EvCem, c.cem.OpEV,
c.cem.OscEV, c.cem.EvSoc,
c.cem.OHPCF,
c.cs.CsLPCInterface, c.cs.CsLPPInterface,
c.ma.MaMGCPInterface, c.ma.MaMPCInterface,
c.ma.MaMGCPInterface, c.ma.MaMPCInterface, c.ma.MaMDTInterface,
c.eg.EgLPCInterface, c.eg.EgLPPInterface,
} {
c.service.AddUseCase(uc)

View file

@ -29,6 +29,11 @@ const (
MPCFrequency uint = 5 // S5 frequency
)
// MDT — Monitoring of Domestic Hot Water Temperature (UC TS v1.0.0)
const (
MDTTemperature uint = 1 // S1 DHW temperature
)
// LPC — Limitation of Power Consumption (UC TS v1.0.0). Same scenario layout for CS and EG roles.
const (
LPCLimit uint = 1 // S1 LoadControl: consumption limit

View file

@ -0,0 +1,27 @@
template: eebus-ohpcf
products:
- description:
de: EEBUS Wärmepumpe (OHPCF)
en: EEBUS heat pump (OHPCF)
group: heating
capabilities: ["meter"]
params:
- preset: eebus
- name: reboost
type: duration
default: 10m
advanced: true
description:
de: Reboost-Intervall
en: Reboost interval
help:
de: Wartezeit, nach der eine neu angekündigte optionale Leistungsaufnahme erneut eingeplant wird.
en: Wait time after which a newly announced optional power consumption is scheduled again.
render: |
type: eebus-ohpcf
ski: {{ .ski }}
{{ if .ip }}ip: {{ .ip }}{{ end }}
reboost: {{ .reboost }}
features:
- heating
- integrateddevice