LG ESS: add battery control (#19630)
This commit is contained in:
parent
b18b7964a8
commit
5ed022aa91
5 changed files with 237 additions and 95 deletions
|
|
@ -11,38 +11,6 @@ import (
|
|||
"github.com/evcc-io/evcc/util"
|
||||
)
|
||||
|
||||
/*
|
||||
This meter supports the LGESS HOME 8, LGESS HOME 10 and LGESS HOME 15 systems from LG with / without battery.
|
||||
|
||||
|
||||
** Usages **
|
||||
The following usages are supported:
|
||||
- grid ... for reading the power imported or exported to the grid
|
||||
- pv ... for reading the power produced by the pv
|
||||
- battery ... for reading the power imported or exported to the battery
|
||||
|
||||
** Example configuration **
|
||||
meters:
|
||||
- name: GridMeter
|
||||
type: template
|
||||
template: lg-ess-home-15
|
||||
usage: grid
|
||||
uri: https://192.168.1.23
|
||||
password: "DE200....."
|
||||
- name: PvMeter
|
||||
type: template
|
||||
template: lg-ess-home-15
|
||||
usage: pv
|
||||
- name: BatteryMeter
|
||||
type: template
|
||||
template: lg-ess-home-15
|
||||
usage: battery
|
||||
|
||||
** Limitations **
|
||||
It is not allowed to provide different URIs or passwords for different lgess meters since always the
|
||||
same hardware instance is accessed with the different usages.
|
||||
*/
|
||||
|
||||
// LgEss implements the api.Meter interface
|
||||
type LgEss struct {
|
||||
usage string // grid, pv, battery
|
||||
|
|
@ -54,7 +22,7 @@ func init() {
|
|||
registry.Add("lgess15", NewLgEss15FromConfig)
|
||||
}
|
||||
|
||||
//go:generate go tool decorate -f decorateLgEss -b *LgEss -r api.Meter -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.Battery,Soc,func() (float64, error)" -t "api.BatteryCapacity,Capacity,func() float64"
|
||||
//go:generate go tool decorate -f decorateLgEss -b *LgEss -r api.Meter -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.Battery,Soc,func() (float64, error)" -t "api.BatteryController,SetBatteryMode,func(api.BatteryMode) error" -t "api.BatteryCapacity,Capacity,func() float64"
|
||||
|
||||
func NewLgEss8FromConfig(other map[string]interface{}) (api.Meter, error) {
|
||||
return NewLgEssFromConfig(other, lgpcs.LgEss8)
|
||||
|
|
@ -70,8 +38,13 @@ func NewLgEssFromConfig(other map[string]interface{}, essType lgpcs.Model) (api.
|
|||
capacity `mapstructure:",squash"`
|
||||
URI, Usage string
|
||||
Registration, Password string
|
||||
battery `mapstructure:",squash"`
|
||||
Cache time.Duration
|
||||
}{
|
||||
battery: battery{
|
||||
MinSoc: 20,
|
||||
MaxSoc: 95,
|
||||
},
|
||||
Cache: time.Second,
|
||||
}
|
||||
|
||||
|
|
@ -83,11 +56,11 @@ func NewLgEssFromConfig(other map[string]interface{}, essType lgpcs.Model) (api.
|
|||
return nil, errors.New("missing usage")
|
||||
}
|
||||
|
||||
return NewLgEss(cc.URI, cc.Usage, cc.Registration, cc.Password, cc.Cache, cc.capacity.Decorator(), essType)
|
||||
return NewLgEss(cc.URI, cc.Usage, cc.Registration, cc.Password, cc.Cache, cc.capacity.Decorator(), cc.battery, essType)
|
||||
}
|
||||
|
||||
// NewLgEss creates an LgEss Meter
|
||||
func NewLgEss(uri, usage, registration, password string, cache time.Duration, capacity func() float64, essType lgpcs.Model) (api.Meter, error) {
|
||||
func NewLgEss(uri, usage, registration, password string, cache time.Duration, capacity func() float64, battery battery, essType lgpcs.Model) (api.Meter, error) {
|
||||
conn, err := lgpcs.GetInstance(uri, registration, password, cache, essType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -106,11 +79,13 @@ func NewLgEss(uri, usage, registration, password string, cache time.Duration, ca
|
|||
|
||||
// decorate api.BatterySoc
|
||||
var batterySoc func() (float64, error)
|
||||
var setBatteryMode func(api.BatteryMode) error
|
||||
if usage == "battery" {
|
||||
batterySoc = m.batterySoc
|
||||
setBatteryMode = m.batteryMode(battery)
|
||||
}
|
||||
|
||||
return decorateLgEss(m, totalEnergy, batterySoc, capacity), nil
|
||||
return decorateLgEss(m, totalEnergy, batterySoc, setBatteryMode, capacity), nil
|
||||
}
|
||||
|
||||
// CurrentPower implements the api.Meter interface
|
||||
|
|
@ -156,3 +131,33 @@ func (m *LgEss) batterySoc() (float64, error) {
|
|||
|
||||
return data.GetBatUserSoc(), nil
|
||||
}
|
||||
|
||||
// batteryMode implements the api.BatteryController interface
|
||||
func (m *LgEss) batteryMode(battery battery) 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(battery.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(battery.MaxSoc), true)
|
||||
default:
|
||||
return api.ErrNotAvailable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import (
|
|||
"github.com/evcc-io/evcc/api"
|
||||
)
|
||||
|
||||
func decorateLgEss(base *LgEss, meterEnergy func() (float64, error), battery func() (float64, error), batteryCapacity func() float64) api.Meter {
|
||||
func decorateLgEss(base *LgEss, meterEnergy func() (float64, error), battery func() (float64, error), batteryController func(api.BatteryMode) error, batteryCapacity func() float64) api.Meter {
|
||||
switch {
|
||||
case battery == nil && meterEnergy == nil:
|
||||
return base
|
||||
|
|
@ -22,7 +22,7 @@ func decorateLgEss(base *LgEss, meterEnergy func() (float64, error), battery fun
|
|||
},
|
||||
}
|
||||
|
||||
case battery != nil && batteryCapacity == nil && meterEnergy == nil:
|
||||
case battery != nil && batteryCapacity == nil && batteryController == nil && meterEnergy == nil:
|
||||
return &struct {
|
||||
*LgEss
|
||||
api.Battery
|
||||
|
|
@ -33,7 +33,7 @@ func decorateLgEss(base *LgEss, meterEnergy func() (float64, error), battery fun
|
|||
},
|
||||
}
|
||||
|
||||
case battery != nil && batteryCapacity == nil && meterEnergy != nil:
|
||||
case battery != nil && batteryCapacity == nil && batteryController == nil && meterEnergy != nil:
|
||||
return &struct {
|
||||
*LgEss
|
||||
api.Battery
|
||||
|
|
@ -48,7 +48,41 @@ func decorateLgEss(base *LgEss, meterEnergy func() (float64, error), battery fun
|
|||
},
|
||||
}
|
||||
|
||||
case battery != nil && batteryCapacity != nil && meterEnergy == nil:
|
||||
case battery != nil && batteryCapacity == nil && batteryController != nil && meterEnergy == nil:
|
||||
return &struct {
|
||||
*LgEss
|
||||
api.Battery
|
||||
api.BatteryController
|
||||
}{
|
||||
LgEss: base,
|
||||
Battery: &decorateLgEssBatteryImpl{
|
||||
battery: battery,
|
||||
},
|
||||
BatteryController: &decorateLgEssBatteryControllerImpl{
|
||||
batteryController: batteryController,
|
||||
},
|
||||
}
|
||||
|
||||
case battery != nil && batteryCapacity == nil && batteryController != nil && meterEnergy != nil:
|
||||
return &struct {
|
||||
*LgEss
|
||||
api.Battery
|
||||
api.BatteryController
|
||||
api.MeterEnergy
|
||||
}{
|
||||
LgEss: base,
|
||||
Battery: &decorateLgEssBatteryImpl{
|
||||
battery: battery,
|
||||
},
|
||||
BatteryController: &decorateLgEssBatteryControllerImpl{
|
||||
batteryController: batteryController,
|
||||
},
|
||||
MeterEnergy: &decorateLgEssMeterEnergyImpl{
|
||||
meterEnergy: meterEnergy,
|
||||
},
|
||||
}
|
||||
|
||||
case battery != nil && batteryCapacity != nil && batteryController == nil && meterEnergy == nil:
|
||||
return &struct {
|
||||
*LgEss
|
||||
api.Battery
|
||||
|
|
@ -63,7 +97,7 @@ func decorateLgEss(base *LgEss, meterEnergy func() (float64, error), battery fun
|
|||
},
|
||||
}
|
||||
|
||||
case battery != nil && batteryCapacity != nil && meterEnergy != nil:
|
||||
case battery != nil && batteryCapacity != nil && batteryController == nil && meterEnergy != nil:
|
||||
return &struct {
|
||||
*LgEss
|
||||
api.Battery
|
||||
|
|
@ -81,6 +115,48 @@ func decorateLgEss(base *LgEss, meterEnergy func() (float64, error), battery fun
|
|||
meterEnergy: meterEnergy,
|
||||
},
|
||||
}
|
||||
|
||||
case battery != nil && batteryCapacity != nil && batteryController != nil && meterEnergy == nil:
|
||||
return &struct {
|
||||
*LgEss
|
||||
api.Battery
|
||||
api.BatteryCapacity
|
||||
api.BatteryController
|
||||
}{
|
||||
LgEss: base,
|
||||
Battery: &decorateLgEssBatteryImpl{
|
||||
battery: battery,
|
||||
},
|
||||
BatteryCapacity: &decorateLgEssBatteryCapacityImpl{
|
||||
batteryCapacity: batteryCapacity,
|
||||
},
|
||||
BatteryController: &decorateLgEssBatteryControllerImpl{
|
||||
batteryController: batteryController,
|
||||
},
|
||||
}
|
||||
|
||||
case battery != nil && batteryCapacity != nil && batteryController != nil && meterEnergy != nil:
|
||||
return &struct {
|
||||
*LgEss
|
||||
api.Battery
|
||||
api.BatteryCapacity
|
||||
api.BatteryController
|
||||
api.MeterEnergy
|
||||
}{
|
||||
LgEss: base,
|
||||
Battery: &decorateLgEssBatteryImpl{
|
||||
battery: battery,
|
||||
},
|
||||
BatteryCapacity: &decorateLgEssBatteryCapacityImpl{
|
||||
batteryCapacity: batteryCapacity,
|
||||
},
|
||||
BatteryController: &decorateLgEssBatteryControllerImpl{
|
||||
batteryController: batteryController,
|
||||
},
|
||||
MeterEnergy: &decorateLgEssMeterEnergyImpl{
|
||||
meterEnergy: meterEnergy,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -102,6 +178,14 @@ func (impl *decorateLgEssBatteryCapacityImpl) Capacity() float64 {
|
|||
return impl.batteryCapacity()
|
||||
}
|
||||
|
||||
type decorateLgEssBatteryControllerImpl struct {
|
||||
batteryController func(api.BatteryMode) error
|
||||
}
|
||||
|
||||
func (impl *decorateLgEssBatteryControllerImpl) SetBatteryMode(p0 api.BatteryMode) error {
|
||||
return impl.batteryController(p0)
|
||||
}
|
||||
|
||||
type decorateLgEssMeterEnergyImpl struct {
|
||||
meterEnergy func() (float64, error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ package lgpcs
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -13,23 +15,18 @@ import (
|
|||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/util/transport"
|
||||
)
|
||||
|
||||
// URIs
|
||||
const (
|
||||
LoginURI = "/v1/login"
|
||||
UserLoginURI = "/v1/user/setting/login"
|
||||
MeterURI = "/v1/user/essinfo/home"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
type Com struct {
|
||||
*request.Helper
|
||||
uri string // URI address of the LG ESS inverter - e.g. "https://192.168.1.28"
|
||||
authPath string
|
||||
password string // registration number of the LG ESS Inverter - e.g. "DE2001..."
|
||||
authKey string // auth_key returned during login and renewed with new login after expiration
|
||||
essType Model // currently the LG Ess Home 8/10 and Home 15 are supported
|
||||
dataG func() (EssData, error)
|
||||
uri string // URI address of the LG ESS inverter - e.g. "https://192.168.1.28"
|
||||
password string // user password, usually MAC address of the LG ESS in lowercase without colons
|
||||
registration string // registration number of the LG ESS Inverter - e.g. "DE2001..."
|
||||
authKey string // auth_key returned during login and renewed with new login after expiration
|
||||
essType Model // currently the LG Ess Home 8/10 and Home 15 are supported
|
||||
dataG func() (EssData, error)
|
||||
log *util.Logger
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
@ -45,16 +42,12 @@ func GetInstance(uri, registration, password string, cache time.Duration, essTyp
|
|||
once.Do(func() {
|
||||
log := util.NewLogger("lgess")
|
||||
instance = &Com{
|
||||
Helper: request.NewHelper(log),
|
||||
uri: uri,
|
||||
authPath: UserLoginURI,
|
||||
password: password,
|
||||
essType: essType,
|
||||
}
|
||||
|
||||
if registration != "" {
|
||||
instance.authPath = LoginURI
|
||||
instance.password = registration
|
||||
Helper: request.NewHelper(log),
|
||||
uri: uri,
|
||||
registration: registration,
|
||||
password: password,
|
||||
essType: essType,
|
||||
log: log,
|
||||
}
|
||||
|
||||
// ignore the self signed certificate
|
||||
|
|
@ -62,24 +55,24 @@ func GetInstance(uri, registration, password string, cache time.Duration, essTyp
|
|||
|
||||
// caches the data access for the "cache" time duration
|
||||
// sends a new request to the pcs if the cache is expired and Data() requested
|
||||
instance.dataG = util.Cached(instance.refreshData, cache)
|
||||
instance.dataG = util.Cached(instance.essInfo, cache)
|
||||
|
||||
// do first login if no authKey exists and uri and password exist
|
||||
if instance.authKey == "" && instance.uri != "" && instance.password != "" {
|
||||
if instance.authKey == "" && instance.uri != "" && (instance.password != "" || instance.registration != "") {
|
||||
err = instance.Login()
|
||||
}
|
||||
})
|
||||
|
||||
// check if different uris are provided
|
||||
if password != "" && registration != "" {
|
||||
return nil, errors.New("cannot have registration and password")
|
||||
}
|
||||
|
||||
// check if different uris are provided
|
||||
if uri != "" && instance.uri != uri {
|
||||
return nil, fmt.Errorf("uri mismatch: %s vs %s", instance.uri, uri)
|
||||
}
|
||||
|
||||
// check if different registrations are provided
|
||||
if registration != "" && instance.registration != registration {
|
||||
return nil, errors.New("registration mismatch")
|
||||
}
|
||||
|
||||
// check if different passwords are provided
|
||||
if password != "" && instance.password != password {
|
||||
return nil, errors.New("password mismatch")
|
||||
|
|
@ -90,11 +83,27 @@ func GetInstance(uri, registration, password string, cache time.Duration, essTyp
|
|||
|
||||
// Login calls login and stores the returned authorization key
|
||||
func (m *Com) Login() error {
|
||||
// check if at least one of password and registration is provided
|
||||
if m.password == "" && m.registration == "" {
|
||||
return errors.New("neither registration nor password provided - at least one needed")
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"password": m.password,
|
||||
}
|
||||
uri := fmt.Sprintf("%s/v1/user/setting/login", m.uri)
|
||||
|
||||
req, err := request.New(http.MethodPut, m.uri+m.authPath, request.MarshalJSON(data), request.JSONEncoding)
|
||||
if m.password == "" { // use installer login
|
||||
m.log.DEBUG.Println("installer login")
|
||||
uri = fmt.Sprintf("%s/v1/login", m.uri)
|
||||
data["password"] = m.registration
|
||||
}
|
||||
|
||||
if m.authKey != "" {
|
||||
m.log.DEBUG.Println("re-login")
|
||||
}
|
||||
|
||||
req, err := request.New(http.MethodPut, uri, request.MarshalJSON(data), request.JSONEncoding)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -125,47 +134,67 @@ func (m *Com) Data() (EssData, error) {
|
|||
return m.dataG()
|
||||
}
|
||||
|
||||
// refreshData reads data from lgess pcs. Tries to re-login if "405" auth_key expired is returned
|
||||
func (m *Com) refreshData() (EssData, error) {
|
||||
// essInfo reads essinfo/home
|
||||
func (m *Com) essInfo() (EssData, error) {
|
||||
f := func(payload any) (*http.Request, error) {
|
||||
uri := fmt.Sprintf("%s/v1/user/essinfo/home", m.uri)
|
||||
return request.New(http.MethodPost, uri, request.MarshalJSON(payload), request.JSONEncoding)
|
||||
}
|
||||
|
||||
if m.essType == LgEss8 {
|
||||
var res MeterResponse8
|
||||
err := m.update(&res)
|
||||
err := m.request(f, nil, &res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
var res MeterResponse15
|
||||
err := m.update(&res)
|
||||
err := m.request(f, nil, &res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (m *Com) update(meterData any) error {
|
||||
data := map[string]interface{}{
|
||||
// BatteryMode sets the battery mode
|
||||
func (m *Com) BatteryMode(mode string, soc int, autocharge bool) error {
|
||||
var res struct{}
|
||||
return m.request(func(payload any) (*http.Request, error) {
|
||||
uri := fmt.Sprintf("%s/v1/user/setting/batt", m.uri)
|
||||
return request.New(http.MethodPut, uri, request.MarshalJSON(payload), request.JSONEncoding)
|
||||
}, map[string]string{
|
||||
"backupmode": mode,
|
||||
"backup_soc": strconv.Itoa(soc),
|
||||
"autocharge": strconv.Itoa(cast.ToInt(autocharge)),
|
||||
}, &res)
|
||||
}
|
||||
|
||||
func (m *Com) request(f func(any) (*http.Request, error), payload map[string]string, res any) error {
|
||||
data := map[string]string{
|
||||
"auth_key": m.authKey,
|
||||
}
|
||||
maps.Copy(data, payload)
|
||||
|
||||
req, err := request.New(http.MethodPost, m.uri+MeterURI, request.MarshalJSON(data), request.JSONEncoding)
|
||||
req, err := f(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.DoJSON(req, meterData); err != nil {
|
||||
// re-login if request returns 405-error
|
||||
if se := new(request.StatusError); errors.As(err, &se) && se.StatusCode() == http.StatusMethodNotAllowed {
|
||||
if err := m.Login(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data["auth_key"] = m.authKey
|
||||
req, err := request.New(http.MethodPost, m.uri+MeterURI, request.MarshalJSON(data), request.JSONEncoding)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.DoJSON(req, &meterData)
|
||||
}
|
||||
err = m.DoJSON(req, &res)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// re-login if request returns 405-error
|
||||
if se := new(request.StatusError); errors.As(err, &se) && se.StatusCode() != http.StatusMethodNotAllowed {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
if err := m.Login(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data["auth_key"] = m.authKey
|
||||
req, err = f(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.DoJSON(req, &res)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue