diff --git a/api/api.go b/api/api.go index df264285b..c7dcafb39 100644 --- a/api/api.go +++ b/api/api.go @@ -222,6 +222,7 @@ type Circuit interface { SetTitle(string) GetParent() Circuit RegisterChild(child Circuit) + Wrap(parent Circuit) error HasMeter() bool GetMaxPower() float64 GetMaxCurrent() float64 diff --git a/api/mock.go b/api/mock.go index eb6f3c7ca..983cfa8d5 100644 --- a/api/mock.go +++ b/api/mock.go @@ -963,3 +963,17 @@ func (mr *MockCircuitMockRecorder) ValidatePower(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidatePower", reflect.TypeOf((*MockCircuit)(nil).ValidatePower), arg0, arg1) } + +// Wrap mocks base method. +func (m *MockCircuit) Wrap(arg0 Circuit) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Wrap", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Wrap indicates an expected call of Wrap. +func (mr *MockCircuitMockRecorder) Wrap(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Wrap", reflect.TypeOf((*MockCircuit)(nil).Wrap), arg0) +} diff --git a/cmd/setup.go b/cmd/setup.go index 86e272339..55fd26305 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -20,6 +20,7 @@ import ( "github.com/evcc-io/evcc/charger" "github.com/evcc-io/evcc/cmd/shutdown" "github.com/evcc-io/evcc/core" + "github.com/evcc-io/evcc/core/circuit" "github.com/evcc-io/evcc/core/keys" "github.com/evcc-io/evcc/hems" "github.com/evcc-io/evcc/meter" @@ -160,7 +161,7 @@ NEXT: } log := util.NewLogger("circuit-" + cc.Name) - instance, err := core.NewCircuitFromConfig(log, cc.Other) + instance, err := circuit.NewFromConfig(log, cc.Other) if err != nil { return fmt.Errorf("cannot create circuit '%s': %w", cc.Name, err) } diff --git a/core/circuit.go b/core/circuit/circuit.go similarity index 89% rename from core/circuit.go rename to core/circuit/circuit.go index b19991449..cef0187c1 100644 --- a/core/circuit.go +++ b/core/circuit/circuit.go @@ -1,4 +1,4 @@ -package core +package circuit import ( "fmt" @@ -34,8 +34,8 @@ type Circuit struct { powerUpdated time.Time } -// NewCircuitFromConfig creates a new Circuit -func NewCircuitFromConfig(log *util.Logger, other map[string]interface{}) (api.Circuit, error) { +// NewFromConfig creates a new Circuit +func NewFromConfig(log *util.Logger, other map[string]interface{}) (api.Circuit, error) { cc := struct { Title string `mapstructure:"title"` // title ParentRef string `mapstructure:"parent"` // parent circuit reference @@ -60,7 +60,7 @@ func NewCircuitFromConfig(log *util.Logger, other map[string]interface{}) (api.C meter = dev.Instance() } - circuit, err := NewCircuit(log, cc.Title, cc.MaxCurrent, cc.MaxPower, meter, cc.Timeout) + circuit, err := New(log, cc.Title, cc.MaxCurrent, cc.MaxPower, meter, cc.Timeout) if err != nil { return nil, err } @@ -70,14 +70,14 @@ func NewCircuitFromConfig(log *util.Logger, other map[string]interface{}) (api.C if err != nil { return nil, err } - circuit.SetParent(dev.Instance()) + circuit.setParent(dev.Instance()) } return circuit, err } -// NewCircuit creates a circuit -func NewCircuit(log *util.Logger, title string, maxCurrent, maxPower float64, meter api.Meter, timeout time.Duration) (*Circuit, error) { +// New creates a circuit +func New(log *util.Logger, title string, maxCurrent, maxPower float64, meter api.Meter, timeout time.Duration) (*Circuit, error) { c := &Circuit{ log: log, title: title, @@ -119,14 +119,24 @@ func (c *Circuit) GetParent() api.Circuit { return c.parent } -// SetParent set parent circuit -func (c *Circuit) SetParent(parent api.Circuit) { +// setParent set parent circuit +func (c *Circuit) setParent(parent api.Circuit) error { c.mu.Lock() defer c.mu.Unlock() + if c.parent != nil { + return fmt.Errorf("circuit already has a parent") + } c.parent = parent if parent != nil { parent.RegisterChild(c) } + return nil +} + +// Wrap wraps circuit with parent, keeping the original meter +func (c *Circuit) Wrap(parent api.Circuit) error { + parent.(*Circuit).meter = c.meter + return c.setParent(parent) } // HasMeter returns the max power setting diff --git a/core/circuit_test.go b/core/circuit/circuit_test.go similarity index 96% rename from core/circuit_test.go rename to core/circuit/circuit_test.go index dbcd82a4e..1fecdeab4 100644 --- a/core/circuit_test.go +++ b/core/circuit/circuit_test.go @@ -1,4 +1,4 @@ -package core +package circuit import ( "testing" @@ -61,7 +61,7 @@ func TestCircuitPower(t *testing.T) { circ := func(t *testing.T, ctrl *gomock.Controller, maxP float64) (*Circuit, *api.MockMeter) { m := api.NewMockMeter(ctrl) - c, err := NewCircuit(log, "foo", 0, maxP, m, 0) + c, err := New(log, "foo", 0, maxP, m, 0) require.NoError(t, err) return c, m } @@ -100,7 +100,7 @@ func TestCircuitCurrents(t *testing.T) { api.NewMockMeter(ctrl), api.NewMockPhaseCurrents(ctrl), } - c, err := NewCircuit(log, "foo", maxC, 0, m, 0) + c, err := New(log, "foo", maxC, 0, m, 0) require.NoError(t, err) return c, m } diff --git a/core/site.go b/core/site.go index b6f86f095..f069063bf 100644 --- a/core/site.go +++ b/core/site.go @@ -19,6 +19,7 @@ import ( "github.com/evcc-io/evcc/core/planner" "github.com/evcc-io/evcc/core/prioritizer" "github.com/evcc-io/evcc/core/session" + "github.com/evcc-io/evcc/core/site" "github.com/evcc-io/evcc/core/soc" "github.com/evcc-io/evcc/core/vehicle" "github.com/evcc-io/evcc/push" @@ -54,6 +55,8 @@ type batteryMeasurement struct { Controllable bool `json:"controllable"` } +var _ site.API = (*Site)(nil) + // Site is the main configuration container. A site can host multiple loadpoints. type Site struct { uiChan chan<- util.Param // client push messages diff --git a/core/site/api.go b/core/site/api.go index 13ab7fe8b..2dc9430fc 100644 --- a/core/site/api.go +++ b/core/site/api.go @@ -11,9 +11,6 @@ type API interface { Loadpoints() []loadpoint.API Vehicles() Vehicles - // GetCircuit returns the assigned circuit - GetCircuit() api.Circuit - // Meta GetTitle() string SetTitle(string) @@ -26,6 +23,10 @@ type API interface { GetBatteryMeterRefs() []string SetBatteryMeterRefs([]string) + // circuits + GetCircuit() api.Circuit + SetCircuit(api.Circuit) + // // battery // diff --git a/core/site_api.go b/core/site_api.go index 62ae175ab..55f28d032 100644 --- a/core/site_api.go +++ b/core/site_api.go @@ -137,15 +137,20 @@ func (site *Site) Vehicles() site.Vehicles { return &vehicles{log: site.log} } -// GetCircuit returns the circuit +// GetCircuit returns the root circuit func (site *Site) GetCircuit() api.Circuit { - if site.circuit == nil { - // return untyped nil - return nil - } + site.RLock() + defer site.RUnlock() return site.circuit } +// SetCircuit sets the root circuit +func (site *Site) SetCircuit(circuit api.Circuit) { + site.Lock() + defer site.Unlock() + site.circuit = circuit +} + // GetPrioritySoc returns the PrioritySoc func (site *Site) GetPrioritySoc() float64 { site.RLock() diff --git a/hems/config.go b/hems/config.go index 5de65493f..df32c6b8f 100644 --- a/hems/config.go +++ b/hems/config.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/evcc-io/evcc/core/site" + "github.com/evcc-io/evcc/hems/eebus" "github.com/evcc-io/evcc/hems/semp" "github.com/evcc-io/evcc/server" ) @@ -19,8 +20,8 @@ func NewFromConfig(typ string, other map[string]interface{}, site site.API, http switch strings.ToLower(typ) { case "sma", "shm", "semp": return semp.New(other, site, httpd) - // case "ocpp": - // return ocpp.New(other, site) + case "eebus": + return eebus.New(other, site) default: return nil, errors.New("unknown hems: " + typ) } diff --git a/hems/eebus/eebus.go b/hems/eebus/eebus.go new file mode 100644 index 000000000..12e695ceb --- /dev/null +++ b/hems/eebus/eebus.go @@ -0,0 +1,183 @@ +package eebus + +import ( + "errors" + "sync" + "time" + + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/core/circuit" + "github.com/evcc-io/evcc/core/site" + "github.com/evcc-io/evcc/provider" + "github.com/evcc-io/evcc/server/eebus" + "github.com/evcc-io/evcc/util" +) + +type EEBus struct { + mux sync.RWMutex + log *util.Logger + + *eebus.Connector + uc *eebus.UseCasesCS + + root api.Circuit + + status status + statusUpdated time.Time + + limit *ucapi.LoadLimit // LPC-041 + failsafeLimit float64 + failsafeDuration time.Duration + + heartbeat *provider.Value[struct{}] +} + +// New creates an EEBus HEMS from generic config +func New(other map[string]interface{}, site site.API) (*EEBus, error) { + var cc struct { + Ski string + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + // get root circuit + root := circuit.Root() + if root == nil { + return nil, errors.New("hems requires load management- please configure root circuit") + } + if !root.HasMeter() { + return nil, errors.New("hems requires root circuit to have meter") + } + + // create new root circuit for LPC + lpc, err := circuit.New(util.NewLogger("lpc"), "eebus", 0, 0, nil, time.Minute) + if err != nil { + return nil, err + } + + // wrap old root with new pc parent + if err := root.Wrap(lpc); err != nil { + return nil, err + } + site.SetCircuit(lpc) + + return NewEEBus(cc.Ski, lpc) +} + +// NewEEBus creates EEBus charger +func NewEEBus(ski string, root api.Circuit) (*EEBus, error) { + if eebus.Instance == nil { + return nil, errors.New("eebus not configured") + } + + c := &EEBus{ + log: util.NewLogger("eebus"), + root: root, + uc: eebus.Instance.ControllableSystem(), + Connector: eebus.NewConnector(nil), + heartbeat: provider.NewValue[struct{}](2 * time.Minute), // LPC-031 + } + + if err := eebus.Instance.RegisterDevice(ski, c); err != nil { + return nil, err + } + + if err := c.Wait(90 * time.Second); err != nil { + return c, err + } + + for _, s := range c.uc.LPC.RemoteEntitiesScenarios() { + c.log.DEBUG.Println("LPC RemoteEntitiesScenarios:", s.Scenarios) + } + + for _, s := range c.uc.LPP.RemoteEntitiesScenarios() { + c.log.DEBUG.Println("LPP RemoteEntitiesScenarios:", s.Scenarios) + } + + for _, s := range c.uc.MGCP.RemoteEntitiesScenarios() { + c.log.DEBUG.Println("MGCP RemoteEntitiesScenarios:", s.Scenarios) + } + + return c, nil +} + +func (c *EEBus) Run() { + for range time.Tick(10 * time.Second) { + if err := c.run(); err != nil { + c.log.ERROR.Println(err) + } + } +} + +// TODO check state machine against spec +func (c *EEBus) run() error { + c.mux.RLock() + defer c.mux.RUnlock() + + c.log.TRACE.Println("status:", c.status) + + // check heartbeat + _, heartbeatErr := c.heartbeat.Get() + if heartbeatErr != nil && c.status != StatusFailsafe { + // LPC-914/2 + c.log.WARN.Println("missing heartbeat- entering failsafe mode") + c.setStatusAndLimit(StatusFailsafe, c.failsafeLimit) + + return nil + } + + // TODO + // status init + // status Unlimited/controlled + // status Unlimited/autonomous + + switch c.status { + case StatusUnlimited: + // LPC-914/1 + if c.limit != nil && c.limit.IsActive { + c.log.WARN.Println("active consumption limit") + c.setStatusAndLimit(StatusLimited, c.limit.Value) + } + + case StatusLimited: + // limit updated? + if !c.limit.IsActive { + c.log.WARN.Println("inactive consumption limit") + c.setStatusAndLimit(StatusUnlimited, 0) + break + } + + c.setLimit(c.limit.Value) + + // LPC-914/1 + if d := c.limit.Duration; d > 0 && time.Since(c.statusUpdated) > d { + c.limit = nil + + c.log.DEBUG.Println("limit duration exceeded- return to normal") + c.setStatusAndLimit(StatusUnlimited, 0) + } + + case StatusFailsafe: + // LPC-914/2 + if d := c.failsafeDuration; heartbeatErr == nil && time.Since(c.statusUpdated) > d { + c.log.DEBUG.Println("heartbeat returned and failsafe duration exceeded- return to normal") + c.setStatusAndLimit(StatusUnlimited, 0) + } + } + + return nil +} + +func (c *EEBus) setStatusAndLimit(status status, limit float64) { + c.status = status + c.statusUpdated = time.Now() + + c.setLimit(limit) +} + +func (c *EEBus) setLimit(limit float64) { + c.root.SetMaxPower(limit) +} diff --git a/hems/eebus/events.go b/hems/eebus/events.go new file mode 100644 index 000000000..58b9c81ea --- /dev/null +++ b/hems/eebus/events.go @@ -0,0 +1,158 @@ +package eebus + +import ( + eebusapi "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/usecases/cs/lpc" + spineapi "github.com/enbility/spine-go/api" + "github.com/evcc-io/evcc/server/eebus" +) + +var _ eebus.Device = (*EEBus)(nil) + +// UseCaseEvent implements the eebus.Device interface +func (c *EEBus) UseCaseEvent(_ spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event eebusapi.EventType) { + switch event { + // Load control obligation limit data update received + // + // Use `ConsumptionLimit` to get the current data + // + // Use Case LPC, Scenario 1 + case lpc.DataUpdateLimit: + c.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 + case lpc.WriteApprovalRequired: + c.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 + case lpc.DataUpdateFailsafeConsumptionActivePowerLimit: + c.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 + case lpc.DataUpdateFailsafeDurationMinimum: + c.dataUpdateFailsafeDurationMinimum() + + // Indicates a notify heartbeat event the application should care of. + // E.g. going into or out of the Failsafe state + // + // Use Case LPC, Scenario 3 + case lpc.DataUpdateHeartbeat: + c.dataUpdateHeartbeat() + + // // Load control obligation limit data update received + // // + // // Use `ProductionLimit` to get the current data + // // + // // Use Case LPC, Scenario 1 + // case lpp.DataUpdateLimit: + // c.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 + // case lpp.WriteApprovalRequired: + // c.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 + // case lpp.DataUpdateFailsafeProductionActivePowerLimit: + // c.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 + // case lpp.DataUpdateFailsafeDurationMinimum: + // c.dataUpdateFailsafeDurationMinimum() + + // // Indicates a notify heartbeat event the application should care of. + // // E.g. going into or out of the Failsafe state + // // + // // Use Case LPP, Scenario 3 + // case lpp.DataUpdateHeartbeat: + // c.dataUpdateHeartbeat() + } +} + +func (c *EEBus) dataUpdateLimit() { + limit, err := c.uc.LPC.ConsumptionLimit() + if err != nil { + c.log.ERROR.Println("LPC.ConsumptionLimit:", err) + return + } + + c.mux.Lock() + defer c.mux.Unlock() + + c.limit = &limit +} + +func (c *EEBus) writeApprovalRequired() { + for k, v := range c.uc.LPC.PendingConsumptionLimits() { + c.log.DEBUG.Println("LPC.PendingConsumptionLimit:", k, v) + c.uc.LPC.ApproveOrDenyConsumptionLimit(k, false, "not implemented") + } +} + +func (c *EEBus) dataUpdateFailsafeConsumptionActivePowerLimit() { + limit, _, err := c.uc.LPC.FailsafeConsumptionActivePowerLimit() + if err != nil { + c.log.ERROR.Println("LPC.FailsafeConsumptionActivePowerLimit:", err) + return + } + + c.mux.Lock() + defer c.mux.Unlock() + + c.failsafeLimit = limit +} + +func (c *EEBus) dataUpdateFailsafeDurationMinimum() { + duration, _, err := c.uc.LPC.FailsafeDurationMinimum() + if err != nil { + c.log.ERROR.Println("LPC.FailsafeDurationMinimum:", err) + return + } + + c.mux.Lock() + defer c.mux.Unlock() + + c.failsafeDuration = duration +} + +func (c *EEBus) dataUpdateHeartbeat() { + c.mux.Lock() + defer c.mux.Unlock() + + c.heartbeat.Set(struct{}{}) +} + +// func (c *EEBus)dataUpdateLimit(){} +// func (c *EEBus)writeApprovalRequired(){} +// func (c *EEBus)dataUpdateFailsafeProductionActivePowerLimit(){} +// func (c *EEBus)dataUpdateFailsafeDurationMinimum(){} +// func (c *EEBus)dataUpdateHeartbeat(){} diff --git a/hems/eebus/types.go b/hems/eebus/types.go new file mode 100644 index 000000000..f0a6623e2 --- /dev/null +++ b/hems/eebus/types.go @@ -0,0 +1,9 @@ +package eebus + +type status int + +const ( + StatusUnlimited status = iota + StatusLimited + StatusFailsafe +) diff --git a/meter/eebus.go b/meter/eebus.go new file mode 100644 index 000000000..db81000dd --- /dev/null +++ b/meter/eebus.go @@ -0,0 +1,153 @@ +package meter + +import ( + "errors" + "time" + + eebusapi "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/usecases/ma/mgcp" + spineapi "github.com/enbility/spine-go/api" + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/provider" + "github.com/evcc-io/evcc/server/eebus" + "github.com/evcc-io/evcc/util" +) + +type EEBus struct { + log *util.Logger + + *eebus.Connector + uc *eebus.UseCasesCS + + power, energy *provider.Value[float64] + voltages, currents *provider.Value[[]float64] +} + +func init() { + registry.Add("eebus", NewEEBusFromConfig) +} + +// New creates an EEBus HEMS from generic config +func NewEEBusFromConfig(other map[string]interface{}) (api.Meter, error) { + cc := struct { + Ski string + Timeout time.Duration + }{ + Timeout: 10 * time.Second, + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + return NewEEBus(cc.Ski, cc.Timeout) +} + +// NewEEBus creates EEBus charger +func NewEEBus(ski string, timeout time.Duration) (*EEBus, error) { + if eebus.Instance == nil { + return nil, errors.New("eebus not configured") + } + + c := &EEBus{ + log: util.NewLogger("eebus"), + uc: eebus.Instance.ControllableSystem(), + Connector: eebus.NewConnector(nil), + power: provider.NewValue[float64](timeout), + energy: provider.NewValue[float64](timeout), + voltages: provider.NewValue[[]float64](timeout), + currents: provider.NewValue[[]float64](timeout), + } + + if err := eebus.Instance.RegisterDevice(ski, c); err != nil { + return nil, err + } + + if err := c.Wait(90 * time.Second); err != nil { + return c, err + } + + return c, nil +} + +var _ eebus.Device = (*EEBus)(nil) + +// UseCaseEvent implements the eebus.Device interface +func (c *EEBus) UseCaseEvent(_ spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event eebusapi.EventType) { + switch event { + case mgcp.DataUpdatePower: + c.dataUpdatePower(entity) + case mgcp.DataUpdateEnergyConsumed: + c.dataUpdateEnergyConsumed(entity) + case mgcp.DataUpdateCurrentPerPhase: + c.dataUpdateCurrentPerPhase(entity) + case mgcp.DataUpdateVoltagePerPhase: + c.dataUpdateVoltagePerPhase(entity) + } +} + +func (c *EEBus) dataUpdatePower(entity spineapi.EntityRemoteInterface) { + data, err := c.uc.MGCP.Power(entity) + if err != nil { + c.log.ERROR.Println("MGCP.Power:", err) + return + } + c.power.Set(data) +} + +func (c *EEBus) dataUpdateEnergyConsumed(entity spineapi.EntityRemoteInterface) { + data, err := c.uc.MGCP.EnergyConsumed(entity) + if err != nil { + c.log.ERROR.Println("MGCP.EnergyConsumed:", err) + return + } + c.energy.Set(data) +} + +func (c *EEBus) dataUpdateCurrentPerPhase(entity spineapi.EntityRemoteInterface) { + data, err := c.uc.MGCP.CurrentPerPhase(entity) + if err != nil { + c.log.ERROR.Println("MGCP.CurrentPerPhase:", err) + return + } + c.currents.Set(data) +} + +func (c *EEBus) dataUpdateVoltagePerPhase(entity spineapi.EntityRemoteInterface) { + data, err := c.uc.MGCP.VoltagePerPhase(entity) + if err != nil { + c.log.ERROR.Println("MGCP.VoltagePerPhase:", err) + return + } + c.voltages.Set(data) +} + +func (c *EEBus) CurrentPower() (float64, error) { + return c.power.Get() +} + +func (c *EEBus) TotalEnergy() (float64, error) { + return c.energy.Get() +} + +func (c *EEBus) PhaseCurrents() (float64, float64, float64, error) { + res, err := c.currents.Get() + if err == nil && len(res) != 3 { + err = errors.New("invalid phase currents") + } + if err != nil { + return 0, 0, 0, err + } + return res[0], res[1], res[2], nil +} + +func (c *EEBus) PhaseVoltages() (float64, float64, float64, error) { + res, err := c.voltages.Get() + if err == nil && len(res) != 3 { + err = errors.New("invalid phase voltages") + } + if err != nil { + return 0, 0, 0, err + } + return res[0], res[1], res[2], nil +} diff --git a/provider/cache.go b/provider/cache.go index 99b790b17..081903acc 100644 --- a/provider/cache.go +++ b/provider/cache.go @@ -105,3 +105,39 @@ func (c *cached[T]) shouldRetryWithBackoff() bool { return false } + +// Value is a cacheable value that can expire +type Value[T any] struct { + mux sync.RWMutex + clock clock.Clock + updated time.Time + cache time.Duration + val T +} + +func NewValue[T any](cache time.Duration) *Value[T] { + return &Value[T]{ + clock: clock.New(), + cache: cache, + } +} + +func (v *Value[T]) Get() (T, error) { + v.mux.RLock() + defer v.mux.RUnlock() + + if v.clock.Since(v.updated) > v.cache { + var zero T + return zero, api.ErrTimeout + } + + return v.val, nil +} + +func (v *Value[T]) Set(val T) { + v.mux.Lock() + defer v.mux.Unlock() + + v.val = val + v.updated = v.clock.Now() +} diff --git a/server/eebus/eebus.go b/server/eebus/eebus.go index 72d68d0ff..8598deb53 100644 --- a/server/eebus/eebus.go +++ b/server/eebus/eebus.go @@ -19,6 +19,9 @@ import ( "github.com/enbility/eebus-go/usecases/cem/evsoc" "github.com/enbility/eebus-go/usecases/cem/opev" "github.com/enbility/eebus-go/usecases/cem/oscev" + "github.com/enbility/eebus-go/usecases/cs/lpc" + "github.com/enbility/eebus-go/usecases/cs/lpp" + "github.com/enbility/eebus-go/usecases/ma/mgcp" shipapi "github.com/enbility/ship-go/api" "github.com/enbility/ship-go/mdns" shiputil "github.com/enbility/ship-go/util" @@ -42,11 +45,17 @@ type UseCasesEVSE struct { OpEV ucapi.CemOPEVInterface OscEV ucapi.CemOSCEVInterface } +type UseCasesCS struct { + LPC ucapi.CsLPCInterface + LPP ucapi.CsLPPInterface + MGCP ucapi.MaMGCPInterface +} type EEBus struct { service eebusapi.ServiceInterface - evseUC *UseCasesEVSE + evseUC UseCasesEVSE + csUC UseCasesCS mux sync.Mutex log *util.Logger @@ -133,7 +142,7 @@ func NewServer(other Config) (*EEBus, error) { localEntity := c.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) // evse - c.evseUC = &UseCasesEVSE{ + c.evseUC = UseCasesEVSE{ EvseCC: evsecc.NewEVSECC(localEntity, c.ucCallback), EvCC: evcc.NewEVCC(c.service, localEntity, c.ucCallback), EvCem: evcem.NewEVCEM(c.service, localEntity, c.ucCallback), @@ -142,11 +151,19 @@ func NewServer(other Config) (*EEBus, error) { EvSoc: evsoc.NewEVSOC(localEntity, c.ucCallback), } + // controllable system + c.csUC = UseCasesCS{ + LPC: lpc.NewLPC(localEntity, c.ucCallback), + LPP: lpp.NewLPP(localEntity, c.ucCallback), + MGCP: mgcp.NewMGCP(localEntity, c.ucCallback), + } + // register use cases for _, uc := range []eebusapi.UseCaseInterface{ c.evseUC.EvseCC, c.evseUC.EvCC, c.evseUC.EvCem, c.evseUC.OpEV, c.evseUC.OscEV, c.evseUC.EvSoc, + c.csUC.LPC, c.csUC.LPP, c.csUC.MGCP, } { c.service.AddUseCase(uc) } @@ -172,7 +189,11 @@ func (c *EEBus) RegisterDevice(ski string, device Device) error { } func (c *EEBus) Evse() *UseCasesEVSE { - return c.evseUC + return &c.evseUC +} + +func (c *EEBus) ControllableSystem() *UseCasesCS { + return &c.csUC } func (c *EEBus) Run() { diff --git a/server/http_config_device_handler.go b/server/http_config_device_handler.go index a28cc5502..fabc97117 100644 --- a/server/http_config_device_handler.go +++ b/server/http_config_device_handler.go @@ -8,7 +8,7 @@ import ( "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/charger" - "github.com/evcc-io/evcc/core" + "github.com/evcc-io/evcc/core/circuit" "github.com/evcc-io/evcc/meter" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/config" @@ -233,7 +233,7 @@ func newDeviceHandler(w http.ResponseWriter, r *http.Request) { case templates.Circuit: conf, err = newDevice(class, req, func(_ string, other map[string]interface{}) (api.Circuit, error) { - return core.NewCircuitFromConfig(util.NewLogger("circuit"), other) + return circuit.NewFromConfig(util.NewLogger("circuit"), other) }, config.Circuits()) } @@ -304,7 +304,7 @@ func updateDeviceHandler(w http.ResponseWriter, r *http.Request) { case templates.Circuit: err = updateDevice(id, class, req, func(_ string, other map[string]interface{}) (api.Circuit, error) { - return core.NewCircuitFromConfig(util.NewLogger("circuit"), other) + return circuit.NewFromConfig(util.NewLogger("circuit"), other) }, config.Circuits()) }