Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
173 changes: 143 additions & 30 deletions meter/eebus.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,27 @@ 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) for usage=grid
// Uses MPC (Monitoring & Power Consumption) for all other usages
// Additionally supports LPC (Limitation of Power Consumption)
type EEBus struct {
log *util.Logger

*eebus.Connector
uc *eebus.UseCasesCS

useMGCP bool // true for grid usage (MGCP), false for others (MPC)
power, energy *util.Value[float64]
voltages, currents *util.Value[[]float64]
}
Expand All @@ -27,11 +35,12 @@ 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,
Expand All @@ -41,19 +50,29 @@ 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 for usage="grid", MPC for all other usages
func NewEEBus(ctx context.Context, ski, ip string, usage templates.Usage, timeout time.Duration) (*EEBus, error) {
if eebus.Instance == nil {
return nil, errors.New("eebus not configured")
}

// Use MGCP for grid connection points, MPC for everything else
useMGCP := usage == templates.UsageGrid

useCase := "mpc"
if useMGCP {
useCase = "mgcp"
}

c := &EEBus{
log: util.NewLogger("eebus"),
log: util.NewLogger("eebus-" + useCase),
uc: eebus.Instance.ControllableSystem(),
Connector: eebus.NewConnector(),
useMGCP: useMGCP,
power: util.NewValue[float64](timeout),
energy: util.NewValue[float64](timeout),
voltages: util.NewValue[[]float64](timeout),
Expand All @@ -76,51 +95,114 @@ 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)
if c.useMGCP {
// MGCP events for grid usage
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)
}
} else {
// MPC events for all other usages
switch event {
case mpc.DataUpdatePower:
c.dataUpdatePower(entity)
case mpc.DataUpdateEnergyConsumed:
c.dataUpdateEnergyConsumed(entity)
case mpc.DataUpdateCurrentsPerPhase:
c.dataUpdateCurrentPerPhase(entity)
case mpc.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
var data float64
var err error

if c.useMGCP {
data, err = c.uc.MGCP.Power(entity)
if err != nil {
c.log.ERROR.Println("MGCP.Power:", err)
return
}
} else {
data, err = c.uc.MPC.Power(entity)
if err != nil {
c.log.ERROR.Println("MPC.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
var data float64
var err error

if c.useMGCP {
data, err = c.uc.MGCP.EnergyConsumed(entity)
if err != nil {
c.log.ERROR.Println("MGCP.EnergyConsumed:", err)
return
}
} else {
data, err = c.uc.MPC.EnergyConsumed(entity)
if err != nil {
c.log.ERROR.Println("MPC.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
var data []float64
var err error

if c.useMGCP {
data, err = c.uc.MGCP.CurrentPerPhase(entity)
if err != nil {
c.log.ERROR.Println("MGCP.CurrentPerPhase:", err)
return
}
} else {
data, err = c.uc.MPC.CurrentPerPhase(entity)
if err != nil {
c.log.ERROR.Println("MPC.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
var data []float64
var err error

if c.useMGCP {
data, err = c.uc.MGCP.VoltagePerPhase(entity)
if err != nil {
c.log.ERROR.Println("MGCP.VoltagePerPhase:", err)
return
}
} else {
data, err = c.uc.MPC.VoltagePerPhase(entity)
if err != nil {
c.log.ERROR.Println("MPC.VoltagePerPhase:", err)
return
}
}

c.voltages.Set(data)
}

Expand Down Expand Up @@ -153,3 +235,34 @@ func (c *EEBus) PhaseVoltages() (float64, float64, float64, error) {
}
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
// When dim=true, a default limit is set (4200W per §14a EnWG)
// When dim=false, the limit is removed

var limit float64
if dim {
limit = 4200
}

return c.uc.LPC.SetConsumptionLimit(ucapi.LoadLimit{
Value: limit,
IsActive: dim,
})
}
25 changes: 25 additions & 0 deletions meter/eebus_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package meter

import (
"context"
"testing"

"github.com/evcc-io/evcc/util/test"
)

func TestEEBus(t *testing.T) {
acceptable := []string{
"eebus not configured",
}

values := map[string]any{
"ski": "test-ski",
"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)
}
}
1 change: 1 addition & 0 deletions meter/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ 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
"eebus not configured", // EEBus
}

func TestTemplates(t *testing.T) {
Expand Down
5 changes: 4 additions & 1 deletion server/eebus/eebus.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -50,6 +51,7 @@ type UseCasesCS struct {
LPC ucapi.CsLPCInterface
LPP ucapi.CsLPPInterface
MGCP ucapi.MaMGCPInterface
MPC ucapi.MaMPCInterface
}

type EEBus struct {
Expand Down Expand Up @@ -154,14 +156,15 @@ 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
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.csUC.LPC, c.csUC.LPP, c.csUC.MGCP, c.csUC.MPC,
} {
c.service.AddUseCase(uc)
}
Expand Down
54 changes: 54 additions & 0 deletions templates/definition/meter/eebus.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
template: eebus-meter
products:
- brand: Generic
description:
de: EEBus Netzanschlusspunkt (MGCP)
en: EEBus grid connection point (MGCP)
- brand: Generic
description:
de: EEBus Verbraucher oder Messstelle (MPC)
en: EEBus consumer or metering device (MPC)
requirements:
description:
de: Ein EEBus Verbraucher oder Messstelle mit bekanntem SKI (Subject Key Identifier). Unterstützt werden die Use Cases MGCP (Monitoring of Grid Connection Point), MPC (Monitoring & Power Consumption) und LPC (Limitation of Power Consumption).
en: An EEBus consumer or metering device with a known SKI (Subject Key Identifier). Supports MGCP (grid connection points), MPC (generic power monitoring), and LPC (Limitation of Power Consumption) use cases.
evcc: ["eebus"]
params:
- name: ski
description:
en: SKI (Subject Key Identifier) of the EEBus device
de: SKI (Subject Key Identifier) des EEBus-Geräts
- name: ip
description:
en: IP address (optional)
de: IP-Adresse (optional)
help:
en: The IP address of the EEBus device. If not specified, the device will be discovered automatically via mDNS.
de: Die IP-Adresse des EEBus-Geräts. Falls nicht angegeben, wird das Gerät automatisch über mDNS gefunden.
- name: usage
type: choice
default: grid
values:
- grid
- other
description:
en: Usage type (grid=MGCP, other=MPC)
de: Verwendungsart (grid=MGCP, andere=MPC)
help:
en: |
Determines the EEBus use case:
- grid: Uses MGCP (Monitoring of Grid Connection Point)
- other: Uses MPC (Monitoring & Power Consumption)
de: |
Bestimmt den EEBus Use Case:
- grid: Verwendet MGCP (Monitoring of Grid Connection Point)
- other: Verwendet MPC (Monitoring & Power Consumption)
render: |
type: eebus
ski: {{ .ski }}
{{- if .ip }}
ip: {{ .ip }}
{{- end }}
{{- if .usage }}
usage: {{ .usage }}
{{- end }}
Loading