EEBus meter: add MPC/LPC use cases (#24082)
This commit is contained in:
parent
b8a614ef0a
commit
7079798d09
6 changed files with 240 additions and 36 deletions
171
meter/eebus.go
171
meter/eebus.go
|
|
@ -6,32 +6,59 @@ import (
|
|||
"time"
|
||||
|
||||
eebusapi "github.com/enbility/eebus-go/api"
|
||||
ucapi "github.com/enbility/eebus-go/usecases/api"
|
||||
"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/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
|
||||
uc *eebus.UseCasesCS
|
||||
uc *eebus.UseCasesCS
|
||||
api monitoringAPI
|
||||
|
||||
power, energy *util.Value[float64]
|
||||
voltages, currents *util.Value[[]float64]
|
||||
power *util.Value[float64]
|
||||
energy *util.Value[float64]
|
||||
currents *util.Value[[]float64]
|
||||
voltages *util.Value[[]float64]
|
||||
}
|
||||
|
||||
// monitoringAPI provides a unified interface for MGCP and MPC use cases
|
||||
type monitoringAPI struct {
|
||||
measurements
|
||||
powerEvent eebusapi.EventType
|
||||
energyEvent eebusapi.EventType
|
||||
currentEvent eebusapi.EventType
|
||||
voltageEvent eebusapi.EventType
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// New creates an EEBus HEMS from generic config
|
||||
// NewEEBusFromConfig creates an EEBus meter from generic config
|
||||
func NewEEBusFromConfig(ctx context.Context, other map[string]interface{}) (api.Meter, error) {
|
||||
cc := struct {
|
||||
Ski string
|
||||
Ip string
|
||||
Usage *templates.Usage
|
||||
Timeout time.Duration
|
||||
}{
|
||||
Timeout: 10 * time.Second,
|
||||
|
|
@ -41,23 +68,48 @@ func NewEEBusFromConfig(ctx context.Context, other map[string]interface{}) (api.
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return NewEEBus(ctx, cc.Ski, cc.Ip, cc.Timeout)
|
||||
return NewEEBus(ctx, cc.Ski, cc.Ip, cc.Usage, cc.Timeout)
|
||||
}
|
||||
|
||||
// NewEEBus creates EEBus charger
|
||||
func NewEEBus(ctx context.Context, ski, ip string, timeout time.Duration) (*EEBus, error) {
|
||||
// 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")
|
||||
}
|
||||
|
||||
cs := eebus.Instance.ControllableSystem()
|
||||
|
||||
// Use MGCP only for explicit grid usage, MPC for everything else (default)
|
||||
useCase := "mpc"
|
||||
api := monitoringAPI{
|
||||
measurements: cs.MPC,
|
||||
powerEvent: mpc.DataUpdatePower,
|
||||
energyEvent: mpc.DataUpdateEnergyConsumed,
|
||||
currentEvent: mpc.DataUpdateCurrentsPerPhase,
|
||||
voltageEvent: mpc.DataUpdateVoltagePerPhase,
|
||||
}
|
||||
|
||||
if usage != nil && *usage == templates.UsageGrid {
|
||||
useCase = "mgcp"
|
||||
api = monitoringAPI{
|
||||
measurements: cs.MGCP,
|
||||
powerEvent: mgcp.DataUpdatePower,
|
||||
energyEvent: mgcp.DataUpdateEnergyConsumed,
|
||||
currentEvent: mgcp.DataUpdateCurrentPerPhase,
|
||||
voltageEvent: mgcp.DataUpdateVoltagePerPhase,
|
||||
}
|
||||
}
|
||||
|
||||
c := &EEBus{
|
||||
log: util.NewLogger("eebus"),
|
||||
uc: eebus.Instance.ControllableSystem(),
|
||||
log: util.NewLogger("eebus-" + useCase),
|
||||
uc: cs,
|
||||
api: api,
|
||||
Connector: eebus.NewConnector(),
|
||||
power: util.NewValue[float64](timeout),
|
||||
energy: util.NewValue[float64](timeout),
|
||||
voltages: util.NewValue[[]float64](timeout),
|
||||
currents: util.NewValue[[]float64](timeout),
|
||||
voltages: util.NewValue[[]float64](timeout),
|
||||
}
|
||||
|
||||
if err := eebus.Instance.RegisterDevice(ski, ip, c); err != nil {
|
||||
|
|
@ -76,80 +128,131 @@ 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 {
|
||||
case mgcp.DataUpdatePower:
|
||||
case c.api.powerEvent:
|
||||
c.dataUpdatePower(entity)
|
||||
case mgcp.DataUpdateEnergyConsumed:
|
||||
case c.api.energyEvent:
|
||||
c.dataUpdateEnergyConsumed(entity)
|
||||
case mgcp.DataUpdateCurrentPerPhase:
|
||||
case c.api.currentEvent:
|
||||
c.dataUpdateCurrentPerPhase(entity)
|
||||
case mgcp.DataUpdateVoltagePerPhase:
|
||||
case c.api.voltageEvent:
|
||||
c.dataUpdateVoltagePerPhase(entity)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *EEBus) dataUpdatePower(entity spineapi.EntityRemoteInterface) {
|
||||
data, err := c.uc.MGCP.Power(entity)
|
||||
data, err := c.api.Power(entity)
|
||||
if err != nil {
|
||||
c.log.ERROR.Println("MGCP.Power:", err)
|
||||
c.log.ERROR.Println("Power:", err)
|
||||
return
|
||||
}
|
||||
c.log.TRACE.Printf("Power: %.0fW", data)
|
||||
c.power.Set(data)
|
||||
}
|
||||
|
||||
func (c *EEBus) dataUpdateEnergyConsumed(entity spineapi.EntityRemoteInterface) {
|
||||
data, err := c.uc.MGCP.EnergyConsumed(entity)
|
||||
data, err := c.api.EnergyConsumed(entity)
|
||||
if err != nil {
|
||||
c.log.ERROR.Println("MGCP.EnergyConsumed:", err)
|
||||
c.log.ERROR.Println("EnergyConsumed:", err)
|
||||
return
|
||||
}
|
||||
c.energy.Set(data)
|
||||
c.log.TRACE.Printf("EnergyConsumed: %.1fkWh", data/1000)
|
||||
// Convert Wh to kWh
|
||||
c.energy.Set(data / 1000)
|
||||
}
|
||||
|
||||
func (c *EEBus) dataUpdateCurrentPerPhase(entity spineapi.EntityRemoteInterface) {
|
||||
data, err := c.uc.MGCP.CurrentPerPhase(entity)
|
||||
data, err := c.api.CurrentPerPhase(entity)
|
||||
if err != nil {
|
||||
c.log.ERROR.Println("MGCP.CurrentPerPhase:", err)
|
||||
c.log.ERROR.Println("CurrentPerPhase:", err)
|
||||
return
|
||||
}
|
||||
c.currents.Set(data)
|
||||
}
|
||||
|
||||
func (c *EEBus) dataUpdateVoltagePerPhase(entity spineapi.EntityRemoteInterface) {
|
||||
data, err := c.uc.MGCP.VoltagePerPhase(entity)
|
||||
data, err := c.api.VoltagePerPhase(entity)
|
||||
if err != nil {
|
||||
c.log.ERROR.Println("MGCP.VoltagePerPhase:", err)
|
||||
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) {
|
||||
return c.energy.Get()
|
||||
res, err := c.energy.Get()
|
||||
if err != nil {
|
||||
return 0, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *EEBus) PhaseCurrents() (float64, float64, float64, error) {
|
||||
var _ api.PhaseCurrents = (*EEBus)(nil)
|
||||
|
||||
func (c *EEBus) Currents() (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 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
|
||||
}
|
||||
|
||||
func (c *EEBus) PhaseVoltages() (float64, float64, float64, error) {
|
||||
var _ api.PhaseVoltages = (*EEBus)(nil)
|
||||
|
||||
func (c *EEBus) Voltages() (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 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
|
||||
}
|
||||
|
||||
var _ api.Dimmer = (*EEBus)(nil)
|
||||
|
||||
// Dimmed implements the api.Dimmer interface
|
||||
func (c *EEBus) Dimmed() (bool, error) {
|
||||
limit, err := c.uc.LPC.ConsumptionLimit()
|
||||
if err != nil {
|
||||
// No limit available means not dimmed
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Check if limit is active and has a valid power value
|
||||
return limit.IsActive && limit.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
|
||||
}
|
||||
|
||||
return c.uc.LPC.SetConsumptionLimit(ucapi.LoadLimit{
|
||||
Value: value,
|
||||
IsActive: dim,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
37
meter/eebus_test.go
Normal file
37
meter/eebus_test.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package meter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/evcc-io/evcc/util/test"
|
||||
)
|
||||
|
||||
func TestEEBus(t *testing.T) {
|
||||
acceptable := []string{
|
||||
"eebus not configured",
|
||||
}
|
||||
|
||||
// Test with explicit grid usage (MGCP)
|
||||
values := map[string]any{
|
||||
"ski": "22dd0b546beaa6a720302119c87fc5e0e7ae2e07",
|
||||
"ip": "192.0.2.2",
|
||||
"usage": "grid",
|
||||
"timeout": "10s",
|
||||
}
|
||||
|
||||
if _, err := NewFromConfig(context.TODO(), "eebus", values); err != nil && !test.Acceptable(err, acceptable) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Test without usage parameter (should default to MPC)
|
||||
valuesNoUsage := map[string]any{
|
||||
"ski": "22dd0b546beaa6a720302119c87fc5e0e7ae2e07",
|
||||
"ip": "192.0.2.2",
|
||||
"timeout": "10s",
|
||||
}
|
||||
|
||||
if _, err := NewFromConfig(context.TODO(), "eebus", valuesNoUsage); err != nil && !test.Acceptable(err, acceptable) {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -34,7 +34,8 @@ var acceptable = []string{
|
|||
"no Speedwire ping response for 127.0.0.1", // SMA
|
||||
"no such network interface", // SMA
|
||||
"missing config values: username, password, key", // E3DC
|
||||
"missing token", // HomeAssistant
|
||||
"eebus not configured", // EEBus
|
||||
"missing token", // HomeAssistant
|
||||
}
|
||||
|
||||
func TestTemplates(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import (
|
|||
"github.com/enbility/eebus-go/usecases/cs/lpc"
|
||||
"github.com/enbility/eebus-go/usecases/cs/lpp"
|
||||
"github.com/enbility/eebus-go/usecases/ma/mgcp"
|
||||
"github.com/enbility/eebus-go/usecases/ma/mpc"
|
||||
shipapi "github.com/enbility/ship-go/api"
|
||||
"github.com/enbility/ship-go/mdns"
|
||||
shiputil "github.com/enbility/ship-go/util"
|
||||
|
|
@ -50,6 +51,7 @@ type UseCasesCS struct {
|
|||
LPC ucapi.CsLPCInterface
|
||||
LPP ucapi.CsLPPInterface
|
||||
MGCP ucapi.MaMGCPInterface
|
||||
MPC ucapi.MaMPCInterface
|
||||
}
|
||||
|
||||
type EEBus struct {
|
||||
|
|
@ -154,6 +156,7 @@ func NewServer(other Config) (*EEBus, error) {
|
|||
LPC: lpc.NewLPC(localEntity, c.ucCallback),
|
||||
LPP: lpp.NewLPP(localEntity, c.ucCallback),
|
||||
MGCP: mgcp.NewMGCP(localEntity, c.ucCallback),
|
||||
MPC: mpc.NewMPC(localEntity, c.ucCallback),
|
||||
}
|
||||
|
||||
// register use cases
|
||||
|
|
@ -161,7 +164,7 @@ func NewServer(other Config) (*EEBus, error) {
|
|||
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.csUC.LPC, c.csUC.LPP, c.csUC.MGCP, c.csUC.MPC,
|
||||
} {
|
||||
c.service.AddUseCase(uc)
|
||||
}
|
||||
|
|
|
|||
30
templates/definition/meter/eebus-mcp.yaml
Normal file
30
templates/definition/meter/eebus-mcp.yaml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
template: eebus-mcp
|
||||
products:
|
||||
- description:
|
||||
de: "EEBus: Verbraucher"
|
||||
en: "EEBus: consumer"
|
||||
group: generic
|
||||
requirements:
|
||||
description:
|
||||
de: EEBus-Verbraucher im Hausnetz mit den Use Cases MPC (Monitoring & Power Consumption) und LPC (Limitation of Power Consumption). Kompatbibel mit steuerbaren Verbrauchseinrichtungen (SteuVE) gemäß §14a EnWG.
|
||||
en: EEBus consumer in the home network using use cases MPC (Monitoring & Power Consumption) and LPC (Limitation of Power Consumption).
|
||||
evcc: ["eebus"]
|
||||
params:
|
||||
- name: usage
|
||||
choice: ["charge"]
|
||||
- name: ski
|
||||
description:
|
||||
en: SKI (Subject Key Identifier)
|
||||
de: SKI (Subject Key Identifier)
|
||||
- name: host
|
||||
required: false
|
||||
help:
|
||||
en: If not specified, the device will be discovered automatically via mDNS.
|
||||
de: Falls nicht angegeben, wird das Gerät automatisch über mDNS gefunden.
|
||||
render: |
|
||||
type: eebus
|
||||
ski: {{ .ski }}
|
||||
{{- if .host }}
|
||||
ip: {{ .host }}
|
||||
{{- end }}
|
||||
usage: {{ .usage }}
|
||||
30
templates/definition/meter/eebus-mgcp.yaml
Normal file
30
templates/definition/meter/eebus-mgcp.yaml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
template: eebus-mgcp
|
||||
products:
|
||||
- description:
|
||||
de: "EEBus: Netzanschlusspunkt"
|
||||
en: "EEBus: grid connection point"
|
||||
group: generic
|
||||
requirements:
|
||||
description:
|
||||
de: EEBus-Messstelle am Netzanschlusspunkt mit dem Use Case MGCP (Monitoring of Grid Connection Point).
|
||||
en: EEBus metering device at the grid connection point using use case MGCP (Monitoring of Grid Connection Point).
|
||||
evcc: ["eebus"]
|
||||
params:
|
||||
- name: usage
|
||||
choice: ["grid"]
|
||||
- name: ski
|
||||
description:
|
||||
en: SKI (Subject Key Identifier)
|
||||
de: SKI (Subject Key Identifier)
|
||||
- name: host
|
||||
required: false
|
||||
help:
|
||||
en: If not specified, the device will be discovered automatically via mDNS.
|
||||
de: Falls keine Angabe, wird das Gerät automatisch über mDNS gefunden.
|
||||
render: |
|
||||
type: eebus
|
||||
ski: {{ .ski }}
|
||||
{{- if .host }}
|
||||
ip: {{ .host }}
|
||||
{{- end }}
|
||||
usage: {{ .usage }}
|
||||
Loading…
Add table
Add a link
Reference in a new issue