evcc-io/meter/lgess.go
2026-02-17 11:10:03 +01:00

169 lines
4.6 KiB
Go

package meter
import (
"errors"
"fmt"
"strings"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/meter/lgpcs"
"github.com/evcc-io/evcc/util"
)
// LgEss implements the api.Meter interface
type LgEss struct {
usage string // grid, pv, battery
conn *lgpcs.Com // communication with the lgpcs device
}
func init() {
registry.Add("lgess8", NewLgEss8FromConfig)
registry.Add("lgess15", NewLgEss15FromConfig)
}
//go:generate go tool decorate -f decorateLgEss -b *LgEss -r api.Meter -t api.MeterEnergy,api.BatteryCapacity,api.Battery,api.BatterySocLimiter,api.BatteryPowerLimiter,api.BatteryController
func NewLgEss8FromConfig(other map[string]any) (api.Meter, error) {
return NewLgEssFromConfig(other, lgpcs.LgEss8)
}
func NewLgEss15FromConfig(other map[string]any) (api.Meter, error) {
return NewLgEssFromConfig(other, lgpcs.LgEss15)
}
// NewLgEssFromConfig creates an LgEss Meter from generic config
func NewLgEssFromConfig(other map[string]any, essType lgpcs.Model) (api.Meter, error) {
cc := struct {
batteryCapacity `mapstructure:",squash"`
batterySocLimits `mapstructure:",squash"`
batteryPowerLimits `mapstructure:",squash"`
URI, Usage string
Registration, Password string
Cache time.Duration
}{
batterySocLimits: batterySocLimits{
MinSoc: 20,
MaxSoc: 95,
},
Cache: time.Second,
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
if cc.Usage == "" {
return nil, errors.New("missing usage")
}
return NewLgEss(cc.URI, cc.Usage, cc.Registration, cc.Password, cc.Cache, cc.batteryCapacity.Decorator(), cc.batterySocLimits, cc.batteryPowerLimits, essType)
}
// NewLgEss creates an LgEss Meter
func NewLgEss(uri, usage, registration, password string, cache time.Duration, capacity func() float64, batterySocLimits batterySocLimits, batteryPowerLimits batteryPowerLimits, essType lgpcs.Model) (api.Meter, error) {
conn, err := lgpcs.GetInstance(uri, registration, password, cache, essType)
if err != nil {
return nil, err
}
m := &LgEss{
usage: strings.ToLower(usage),
conn: conn,
}
// decorate api.MeterEnergy
var totalEnergy func() (float64, error)
if m.usage == "grid" && essType != lgpcs.LgEss15 {
totalEnergy = m.totalEnergy
}
// decorate battery
var batterySoc func() (float64, error)
var batterySocLimiter func() (float64, float64)
var setBatteryMode func(api.BatteryMode) error
if usage == "battery" {
batterySoc = m.batterySoc
batterySocLimiter = batterySocLimits.Decorator()
if version, err := conn.GetFirmwareVersion(); err == nil && version >= 7430 {
setBatteryMode = m.batteryMode(batterySocLimits)
}
}
return decorateLgEss(m, totalEnergy, capacity, batterySoc, batterySocLimiter, batteryPowerLimits.Decorator(), setBatteryMode), nil
}
// CurrentPower implements the api.Meter interface
func (m *LgEss) CurrentPower() (float64, error) {
data, err := m.conn.Data()
if err != nil {
return 0, err
}
switch m.usage {
case "grid":
return data.GetGridPower(), nil
case "pv":
return data.GetPvTotalPower(), nil
case "battery":
return data.GetBatConvPower(), nil
default:
return 0, fmt.Errorf("invalid usage: %s", m.usage)
}
}
// totalEnergy implements the api.MeterEnergy interface
func (m *LgEss) totalEnergy() (float64, error) {
data, err := m.conn.Data()
if err != nil {
return 0, err
}
switch m.usage {
case "grid":
return data.GetCurrentGridFeedInEnergy() / 1e3, nil
default:
return 0, fmt.Errorf("invalid usage: %s", m.usage)
}
}
// batterySoc implements the api.Battery interface
func (m *LgEss) batterySoc() (float64, error) {
data, err := m.conn.Data()
if err != nil {
return 0, err
}
return data.GetBatUserSoc(), nil
}
// batteryMode implements the api.BatteryController interface
func (m *LgEss) batteryMode(batterySocLimits batterySocLimits) func(api.BatteryMode) error {
return func(mode api.BatteryMode) error {
switch mode {
case api.BatteryNormal:
// firmeware bug: battery not discharging after hold mode
// if battery is sleeping, wake up with charging for 10sec
m.conn.BatteryMode("on", 100, true)
time.Sleep(10 * time.Second)
// now turn Battery discharge on
return m.conn.BatteryMode("on", int(batterySocLimits.MinSoc), true)
case api.BatteryHold:
soc, err := m.batterySoc()
if err != nil {
return err
}
// soc needs to be the next higher int value to stop discharging immediately
// example: batterySoc=50.7 -> set 51
if int(soc)+1 < 100 {
soc++
}
return m.conn.BatteryMode("on", int(soc), true)
case api.BatteryCharge:
return m.conn.BatteryMode("on", int(batterySocLimits.MaxSoc), true)
default:
return api.ErrNotAvailable
}
}
}