Add EEBus HEMS for SteuVE (#14950)
This commit is contained in:
parent
f9501e53de
commit
6c3ecdb6ff
16 changed files with 625 additions and 29 deletions
|
|
@ -222,6 +222,7 @@ type Circuit interface {
|
|||
SetTitle(string)
|
||||
GetParent() Circuit
|
||||
RegisterChild(child Circuit)
|
||||
Wrap(parent Circuit) error
|
||||
HasMeter() bool
|
||||
GetMaxPower() float64
|
||||
GetMaxCurrent() float64
|
||||
|
|
|
|||
14
api/mock.go
14
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
//
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
183
hems/eebus/eebus.go
Normal file
183
hems/eebus/eebus.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
158
hems/eebus/events.go
Normal file
158
hems/eebus/events.go
Normal file
|
|
@ -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(){}
|
||||
9
hems/eebus/types.go
Normal file
9
hems/eebus/types.go
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package eebus
|
||||
|
||||
type status int
|
||||
|
||||
const (
|
||||
StatusUnlimited status = iota
|
||||
StatusLimited
|
||||
StatusFailsafe
|
||||
)
|
||||
153
meter/eebus.go
Normal file
153
meter/eebus.go
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue