Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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: 2 additions & 6 deletions hems/eebus/eebus.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,8 @@ func NewEEBus(ctx context.Context, ski string, limits Limits, passthrough func(b
}

// controllable system
for _, s := range c.cs.CsLPCInterface.RemoteEntitiesScenarios() {
c.log.DEBUG.Printf("ski %s CS LPC scenarios: %v", s.Entity.Device().Ski(), s.Scenarios)
}
for _, s := range c.cs.CsLPPInterface.RemoteEntitiesScenarios() {
c.log.DEBUG.Printf("ski %s CS LPP scenarios: %v", s.Entity.Device().Ski(), s.Scenarios)
}
eebus.LogEntities(c.log.DEBUG, "CS LPC", c.cs.CsLPCInterface)
eebus.LogEntities(c.log.DEBUG, "CS LPP", c.cs.CsLPPInterface)

// set initial values
if err := c.cs.CsLPCInterface.SetConsumptionNominalMax(limits.ContractualConsumptionNominalMax); err != nil {
Expand Down
244 changes: 68 additions & 176 deletions meter/eebus.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,12 @@ import (
"context"
"errors"
"fmt"
"slices"
"strings"
"sync"
"time"

eebusapi "github.com/enbility/eebus-go/api"
ucapi "github.com/enbility/eebus-go/usecases/api"
"github.com/enbility/eebus-go/usecases/eg/lpc"
"github.com/enbility/eebus-go/usecases/eg/lpp"
"github.com/enbility/eebus-go/usecases/ma/mgcp"
"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"
Expand All @@ -35,19 +30,14 @@ type EEBus struct {
eg *eebus.EnergyGuard
mm measurements

power *util.Value[float64]
energy *util.Value[float64]
currents *util.Value[[]float64]
voltages *util.Value[[]float64]

mu sync.Mutex
consumptionLimit ucapi.LoadLimit
productionLimit ucapi.LoadLimit
egLpcEntity spineapi.EntityRemoteInterface
egLppEntity spineapi.EntityRemoteInterface
mu sync.Mutex
maEntity spineapi.EntityRemoteInterface
egLpcEntity spineapi.EntityRemoteInterface
egLppEntity spineapi.EntityRemoteInterface
}

type measurements interface {
eebusapi.UseCaseBaseInterface
Power(entity spineapi.EntityRemoteInterface) (float64, error)
EnergyConsumed(entity spineapi.EntityRemoteInterface) (float64, error)
CurrentPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error)
Expand All @@ -60,25 +50,23 @@ func init() {

// NewEEBusFromConfig creates an EEBus meter from generic config
func NewEEBusFromConfig(ctx context.Context, other map[string]any) (api.Meter, error) {
cc := struct {
Ski string
Ip string
Usage *templates.Usage
Timeout time.Duration
}{
Timeout: 10 * time.Second,
var cc struct {
Ski string
Ip string
Usage *templates.Usage
Timeout_ time.Duration `mapstructure:"timeout"`
}

if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}

return NewEEBus(ctx, cc.Ski, cc.Ip, cc.Usage, cc.Timeout)
return NewEEBus(ctx, cc.Ski, cc.Ip, cc.Usage)
}

// NewEEBus creates an EEBus meter
// Uses MGCP only when usage="grid", otherwise uses MPC (default)
func NewEEBus(ctx context.Context, ski, ip string, usage *templates.Usage, timeout time.Duration) (api.Meter, error) {
func NewEEBus(ctx context.Context, ski, ip string, usage *templates.Usage) (api.Meter, error) {
if eebus.Instance == nil {
return nil, errors.New("eebus not configured")
}
Expand All @@ -100,10 +88,6 @@ func NewEEBus(ctx context.Context, ski, ip string, usage *templates.Usage, timeo
eg: eebus.Instance.EnergyGuard(),
mm: mm,
Connector: eebus.NewConnector(),
power: util.NewValue[float64](timeout),
energy: util.NewValue[float64](timeout),
currents: util.NewValue[[]float64](timeout),
voltages: util.NewValue[[]float64](timeout),
}

if err := eebus.Instance.RegisterDevice(ski, ip, c); err != nil {
Expand All @@ -116,157 +100,87 @@ func NewEEBus(ctx context.Context, ski, ip string, usage *templates.Usage, timeo
}

// monitoring appliance
for _, s := range c.ma.MaMPCInterface.RemoteEntitiesScenarios() {
c.log.DEBUG.Printf("ski %s MA MPC scenarios: %v", s.Entity.Device().Ski(), s.Scenarios)
}
for _, s := range c.ma.MaMGCPInterface.RemoteEntitiesScenarios() {
c.log.DEBUG.Printf("ski %s MA MGCP scenarios: %v", s.Entity.Device().Ski(), s.Scenarios)
}
eebus.LogEntities(c.log.DEBUG, "MA MPC", c.ma.MaMPCInterface)
eebus.LogEntities(c.log.DEBUG, "MA MGCP", c.ma.MaMGCPInterface)

// energy guard
for _, s := range c.eg.EgLPCInterface.RemoteEntitiesScenarios() {
c.log.DEBUG.Printf("ski %s EG LPC scenarios: %v", s.Entity.Device().Ski(), s.Scenarios)
}
for _, s := range c.eg.EgLPPInterface.RemoteEntitiesScenarios() {
c.log.DEBUG.Printf("ski %s EG LPP scenarios: %v", s.Entity.Device().Ski(), s.Scenarios)
}
eebus.LogEntities(c.log.DEBUG, "EG LPC", c.eg.EgLPCInterface)
eebus.LogEntities(c.log.DEBUG, "EG LPP", c.eg.EgLPPInterface)

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 {
// Monitoring Appliance
case mpc.DataUpdatePower, mgcp.DataUpdatePower:
c.maDataUpdatePower(entity)
case mpc.DataUpdateEnergyConsumed, mgcp.DataUpdateEnergyConsumed:
c.maDataUpdateEnergyConsumed(entity)
case mpc.DataUpdateCurrentsPerPhase, mgcp.DataUpdateCurrentPerPhase:
c.maDataUpdateCurrentPerPhase(entity)
case mpc.DataUpdateVoltagePerPhase, mgcp.DataUpdateVoltagePerPhase:
c.maDataUpdateVoltagePerPhase(entity)

// Energy Guard - LPC
case lpc.UseCaseSupportUpdate:
c.egLpcUseCaseSupportUpdate(entity)
case lpc.DataUpdateLimit:
c.egLpcDataUpdateLimit(entity)

// Energy Guard - LPP
case lpp.UseCaseSupportUpdate:
c.egLppUseCaseSupportUpdate(entity)
case lpp.DataUpdateLimit:
c.egLppDataUpdateLimit(entity)
}
}
func eebusReadValue[T any](scenario uint, uc eebusapi.UseCaseBaseInterface, entity spineapi.EntityRemoteInterface, update func(entity spineapi.EntityRemoteInterface) (T, error)) (T, error) {
var zero T

func (c *EEBus) maDataUpdatePower(entity spineapi.EntityRemoteInterface) {
data, err := c.mm.Power(entity)
if err != nil {
c.log.ERROR.Println("Power:", err)
return
if entity == nil || !uc.IsScenarioAvailableAtEntity(entity, scenario) {
return zero, api.ErrNotAvailable
}
c.log.TRACE.Printf("Power: %.0fW", data)
c.power.Set(data)
}

func (c *EEBus) maDataUpdateEnergyConsumed(entity spineapi.EntityRemoteInterface) {
data, err := c.mm.EnergyConsumed(entity)
res, err := update(entity)
if err != nil {
c.log.ERROR.Println("EnergyConsumed:", err)
return
// announced but not provided
if errors.Is(err, eebusapi.ErrDataNotAvailable) {
err = api.ErrNotAvailable
}
return zero, err
}
c.log.TRACE.Printf("EnergyConsumed: %.1fkWh", data/1000)
// Convert Wh to kWh
c.energy.Set(data / 1000)
}

func (c *EEBus) maDataUpdateCurrentPerPhase(entity spineapi.EntityRemoteInterface) {
data, err := c.mm.CurrentPerPhase(entity)
if err != nil {
c.log.ERROR.Println("CurrentPerPhase:", err)
return
}
c.currents.Set(data)
return res, nil
}

func (c *EEBus) maDataUpdateVoltagePerPhase(entity spineapi.EntityRemoteInterface) {
data, err := c.mm.VoltagePerPhase(entity)
if err != nil {
c.log.ERROR.Println("VoltagePerPhase:", err)
return
}
c.voltages.Set(data)
func (c *EEBus) readValue(scenario uint, update func(entity spineapi.EntityRemoteInterface) (float64, error)) (float64, error) {
c.mu.Lock()
defer c.mu.Unlock()
return eebusReadValue(scenario, c.mm, c.maEntity, update)
}

var _ api.Meter = (*EEBus)(nil)

func (c *EEBus) CurrentPower() (float64, error) {
return c.power.Get()
return c.readValue(1, c.mm.Power)
}

var _ api.MeterEnergy = (*EEBus)(nil)

func (c *EEBus) TotalEnergy() (float64, error) {
res, err := c.energy.Get()
if err != nil {
return 0, api.ErrNotAvailable
}

return res, nil
return c.readValue(2, c.mm.EnergyConsumed)
}

var _ api.PhaseCurrents = (*EEBus)(nil)
func (c *EEBus) readPhases(scenario uint, update func(entity spineapi.EntityRemoteInterface) ([]float64, error)) (float64, float64, float64, error) {
c.mu.Lock()
defer c.mu.Unlock()

func (c *EEBus) Currents() (float64, float64, float64, error) {
res, err := c.currents.Get()
res, err := eebusReadValue(scenario, c.mm, c.maEntity, update)
if err != nil {
return 0, 0, 0, api.ErrNotAvailable
}
if len(res) != 3 {
return 0, 0, 0, errors.New("invalid phase currents")
// announced but not provided
if errors.Is(err, eebusapi.ErrDataNotAvailable) {
err = api.ErrNotAvailable
}
return 0, 0, 0, err
}
return res[0], res[1], res[2], nil
}

var _ api.PhaseVoltages = (*EEBus)(nil)

func (c *EEBus) Voltages() (float64, float64, float64, error) {
res, err := c.voltages.Get()
if err != nil {
return 0, 0, 0, api.ErrNotAvailable
if len(res) < 1 || len(res) > 3 {
return 0, 0, 0, fmt.Errorf("invalid phases: %v", res)
}
if len(res) != 3 {
return 0, 0, 0, errors.New("invalid phase voltages")

for len(res) < 3 {
res = append(res, 0)
}

return res[0], res[1], res[2], nil
}

//
// Energy Guard - LPC
//

func (c *EEBus) egLpcUseCaseSupportUpdate(entity spineapi.EntityRemoteInterface) {
c.mu.Lock()
defer c.mu.Unlock()
var _ api.PhaseCurrents = (*EEBus)(nil)

c.egLpcEntity = entity
func (c *EEBus) Currents() (float64, float64, float64, error) {
return c.readPhases(3, c.mm.CurrentPerPhase)
}

func (c *EEBus) egLpcDataUpdateLimit(entity spineapi.EntityRemoteInterface) {
limit, err := c.eg.EgLPCInterface.ConsumptionLimit(entity)
if err != nil {
c.log.ERROR.Println("EG LPC ConsumptionLimit:", err)
return
}

c.mu.Lock()
defer c.mu.Unlock()
var _ api.PhaseVoltages = (*EEBus)(nil)

c.consumptionLimit = limit
func (c *EEBus) Voltages() (float64, float64, float64, error) {
return c.readPhases(4, c.mm.VoltagePerPhase)
}

var _ api.Dimmer = (*EEBus)(nil)
Expand All @@ -276,8 +190,13 @@ func (c *EEBus) Dimmed() (bool, error) {
c.mu.Lock()
defer c.mu.Unlock()

limit, err := eebusReadValue(1, c.eg.EgLPCInterface, c.egLpcEntity, c.eg.EgLPCInterface.ConsumptionLimit)
if err != nil {
return false, err
}

// Check if limit is active and has a valid power value
return c.consumptionLimit.IsActive && c.consumptionLimit.Value > 0, nil
return limit.IsActive && limit.Value > 0, nil
}

// Dim implements the api.Dimmer interface
Expand All @@ -296,14 +215,10 @@ func (c *EEBus) Dim(dim bool) error {
c.mu.Lock()
defer c.mu.Unlock()

if c.egLpcEntity == nil {
if c.egLpcEntity == nil || !c.eg.EgLPCInterface.IsScenarioAvailableAtEntity(c.egLpcEntity, 1) {
return api.ErrNotAvailable
}

if !slices.Contains(c.eg.EgLPCInterface.AvailableScenariosForEntity(c.egLpcEntity), 1) {
return errors.New("eg lpc: scenario 1 not supported")
}

_, err := c.eg.EgLPCInterface.WriteConsumptionLimit(c.egLpcEntity, ucapi.LoadLimit{
Value: value,
IsActive: dim,
Expand All @@ -312,39 +227,20 @@ func (c *EEBus) Dim(dim bool) error {
return err
}

//
// Energy Guard - LPP
//

func (c *EEBus) egLppUseCaseSupportUpdate(entity spineapi.EntityRemoteInterface) {
c.mu.Lock()
defer c.mu.Unlock()

c.egLppEntity = entity
}

func (c *EEBus) egLppDataUpdateLimit(entity spineapi.EntityRemoteInterface) {
limit, err := c.eg.EgLPPInterface.ProductionLimit(entity)
if err != nil {
c.log.ERROR.Println("EG LPP ProductionLimit:", err)
return
}

c.mu.Lock()
defer c.mu.Unlock()

c.productionLimit = limit
}

var _ api.Curtailer = (*EEBus)(nil)

// Curtailed implements the api.Curtailer interface
func (c *EEBus) Curtailed() (bool, error) {
c.mu.Lock()
defer c.mu.Unlock()

limit, err := eebusReadValue(1, c.eg.EgLPPInterface, c.egLppEntity, c.eg.EgLPPInterface.ProductionLimit)
if err != nil {
return false, err
}

// Check if limit is active and has a valid power value
return c.productionLimit.IsActive && c.productionLimit.Value > 0, nil
return limit.IsActive && limit.Value > 0, nil
}

// Curtail implements the api.Curtailer interface
Expand All @@ -363,14 +259,10 @@ func (c *EEBus) Curtail(curtail bool) error {
c.mu.Lock()
defer c.mu.Unlock()

if c.egLppEntity == nil {
if c.egLppEntity == nil || !c.eg.EgLPPInterface.IsScenarioAvailableAtEntity(c.egLppEntity, 1) {
return api.ErrNotAvailable
}

if !slices.Contains(c.eg.EgLPPInterface.AvailableScenariosForEntity(c.egLppEntity), 1) {
return errors.New("eg lpp: scenario 1 not supported")
}

_, err := c.eg.EgLPPInterface.WriteProductionLimit(c.egLppEntity, ucapi.LoadLimit{
Value: value,
IsActive: curtail,
Expand Down
Loading
Loading