298 lines
7.6 KiB
Go
298 lines
7.6 KiB
Go
package meter
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"slices"
|
|
"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/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"
|
|
"github.com/evcc-io/evcc/server/eebus"
|
|
"github.com/evcc-io/evcc/util"
|
|
"github.com/evcc-io/evcc/util/templates"
|
|
)
|
|
|
|
// EEBus is an EEBus meter implementation supporting MGCP, MPC, and LPC use cases
|
|
// Uses MGCP (Monitoring of Grid Connection Point) only when usage="grid"
|
|
// Uses MPC (Monitoring & Power Consumption) for all other cases (default)
|
|
// Additionally supports LPC (Limitation of Power Consumption)
|
|
type EEBus struct {
|
|
log *util.Logger
|
|
|
|
*eebus.Connector
|
|
ma *eebus.MonitoringAppliance
|
|
eg *eebus.EnergyGuard
|
|
mm measurements
|
|
|
|
power *util.Value[float64]
|
|
energy *util.Value[float64]
|
|
currents *util.Value[[]float64]
|
|
voltages *util.Value[[]float64]
|
|
|
|
// TODO use util.Value
|
|
mu sync.Mutex
|
|
consumptionLimit *ucapi.LoadLimit
|
|
egLpcEntity spineapi.EntityRemoteInterface
|
|
// failsafeLimit float64
|
|
// failsafeDuration time.Duration
|
|
}
|
|
|
|
type measurements interface {
|
|
Power(entity spineapi.EntityRemoteInterface) (float64, error)
|
|
EnergyConsumed(entity spineapi.EntityRemoteInterface) (float64, error)
|
|
CurrentPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error)
|
|
VoltagePerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error)
|
|
}
|
|
|
|
func init() {
|
|
registry.AddCtx("eebus", NewEEBusFromConfig)
|
|
}
|
|
|
|
// 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,
|
|
}
|
|
|
|
if err := util.DecodeOther(other, &cc); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return NewEEBus(ctx, cc.Ski, cc.Ip, cc.Usage, cc.Timeout)
|
|
}
|
|
|
|
// 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) {
|
|
if eebus.Instance == nil {
|
|
return nil, errors.New("eebus not configured")
|
|
}
|
|
|
|
ma := eebus.Instance.MonitoringAppliance()
|
|
|
|
// Use MGCP only for explicit grid usage, MPC for everything else (default)
|
|
useCase := "mpc"
|
|
mm := measurements(ma.MaMPCInterface)
|
|
|
|
if usage != nil && *usage == templates.UsageGrid {
|
|
useCase = "mgcp"
|
|
mm = ma.MaMGCPInterface
|
|
}
|
|
|
|
c := &EEBus{
|
|
log: util.NewLogger("eebus-" + useCase),
|
|
ma: ma,
|
|
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 {
|
|
return nil, err
|
|
}
|
|
|
|
if err := c.Wait(ctx); err != nil {
|
|
eebus.Instance.UnregisterDevice(ski, c)
|
|
return nil, 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) {
|
|
c.log.TRACE.Printf("recv: %s", event)
|
|
|
|
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
|
|
case lpc.UseCaseSupportUpdate:
|
|
c.egLpcUseCaseSupportUpdate(entity)
|
|
case lpc.DataUpdateLimit:
|
|
c.egLpcDataUpdateLimit(entity)
|
|
}
|
|
}
|
|
|
|
func (c *EEBus) maDataUpdatePower(entity spineapi.EntityRemoteInterface) {
|
|
data, err := c.mm.Power(entity)
|
|
if err != nil {
|
|
c.log.ERROR.Println("Power:", err)
|
|
return
|
|
}
|
|
c.log.TRACE.Printf("Power: %.0fW", data)
|
|
c.power.Set(data)
|
|
}
|
|
|
|
func (c *EEBus) maDataUpdateEnergyConsumed(entity spineapi.EntityRemoteInterface) {
|
|
data, err := c.mm.EnergyConsumed(entity)
|
|
if err != nil {
|
|
c.log.ERROR.Println("EnergyConsumed:", err)
|
|
return
|
|
}
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
var _ api.Meter = (*EEBus)(nil)
|
|
|
|
func (c *EEBus) CurrentPower() (float64, error) {
|
|
return c.power.Get()
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
var _ api.PhaseCurrents = (*EEBus)(nil)
|
|
|
|
func (c *EEBus) Currents() (float64, float64, float64, error) {
|
|
res, err := c.currents.Get()
|
|
if err != nil {
|
|
return 0, 0, 0, api.ErrNotAvailable
|
|
}
|
|
if len(res) != 3 {
|
|
return 0, 0, 0, errors.New("invalid phase currents")
|
|
}
|
|
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) != 3 {
|
|
return 0, 0, 0, errors.New("invalid phase voltages")
|
|
}
|
|
return res[0], res[1], res[2], nil
|
|
}
|
|
|
|
//
|
|
// Energy Guard
|
|
//
|
|
|
|
func (c *EEBus) egLpcUseCaseSupportUpdate(entity spineapi.EntityRemoteInterface) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
c.egLpcEntity = entity
|
|
}
|
|
|
|
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()
|
|
|
|
c.consumptionLimit = &limit
|
|
}
|
|
|
|
var _ api.Dimmer = (*EEBus)(nil)
|
|
|
|
// Dimmed implements the api.Dimmer interface
|
|
func (c *EEBus) Dimmed() (bool, error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
// Check if limit is active and has a valid power value
|
|
return c.consumptionLimit != nil && c.consumptionLimit.IsActive && c.consumptionLimit.Value > 0, nil
|
|
}
|
|
|
|
// Dim implements the api.Dimmer interface
|
|
func (c *EEBus) Dim(dim bool) error {
|
|
// Sets or removes the consumption power limit
|
|
|
|
// TODO: change api.Dimmer to make limit configurable
|
|
// For now, we use a fixed safe limit of 0W
|
|
limit := 0.0
|
|
|
|
var value float64
|
|
if dim {
|
|
value = limit
|
|
}
|
|
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
if c.egLpcEntity == nil {
|
|
return errors.New("not connected")
|
|
}
|
|
|
|
if !slices.Contains(c.eg.EgLPCInterface.AvailableScenariosForEntity(c.egLpcEntity), 1) {
|
|
return errors.New("scenario 1 not supported")
|
|
}
|
|
|
|
_, err := c.eg.EgLPCInterface.WriteConsumptionLimit(c.egLpcEntity, ucapi.LoadLimit{
|
|
Value: value,
|
|
IsActive: dim,
|
|
}, func(result model.ResultDataType) {
|
|
if result.ErrorNumber != nil {
|
|
c.log.ERROR.Println("ErrorNumber", *result.ErrorNumber)
|
|
}
|
|
if result.Description != nil {
|
|
c.log.ERROR.Println("Description", *result.Description)
|
|
}
|
|
})
|
|
|
|
return err
|
|
}
|