Skip to content

Commit

Permalink
Add LG ESS Home 15 (evcc-io#17484)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig authored and jonilala796 committed Jan 3, 2025
1 parent 31cf87b commit f6c9a74
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 65 deletions.
53 changes: 33 additions & 20 deletions meter/lgess.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import (
)

/*
This meter supports the LGESS HOME 8 and LGESS HOME 10 systems from LG with / without battery.
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:
Expand All @@ -23,15 +24,18 @@ The following usages are supported:
** Example configuration **
meters:
- name: GridMeter
type: lgess
type: template
template: lg-ess-home-15
usage: grid
uri: https://192.168.1.23
password: "DE200....."
- name: PvMeter
type: lgess
type: template
template: lg-ess-home-15
usage: pv
- name: BatteryMeter
type: lgess
type: template
template: lg-ess-home-15
usage: battery
** Limitations **
Expand All @@ -42,17 +46,26 @@ same hardware instance is accessed with the different usages.
// LgEss implements the api.Meter interface
type LgEss struct {
usage string // grid, pv, battery
lp *lgpcs.Com // communication with the lgpcs device
conn *lgpcs.Com // communication with the lgpcs device
}

func init() {
registry.Add("lgess", NewLgEssFromConfig)
registry.Add("lgess8", NewLgEss8FromConfig)
registry.Add("lgess15", NewLgEss15FromConfig)
}

//go:generate go run ../cmd/tools/decorate.go -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"

func NewLgEss8FromConfig(other map[string]interface{}) (api.Meter, error) {
return NewLgEssFromConfig(other, lgpcs.LgEss8)
}

func NewLgEss15FromConfig(other map[string]interface{}) (api.Meter, error) {
return NewLgEssFromConfig(other, lgpcs.LgEss15)
}

// NewLgEssFromConfig creates an LgEss Meter from generic config
func NewLgEssFromConfig(other map[string]interface{}) (api.Meter, error) {
func NewLgEssFromConfig(other map[string]interface{}, essType lgpcs.Model) (api.Meter, error) {
cc := struct {
capacity `mapstructure:",squash"`
URI, Usage string
Expand All @@ -70,24 +83,24 @@ func NewLgEssFromConfig(other map[string]interface{}) (api.Meter, error) {
return nil, errors.New("missing usage")
}

return NewLgEss(cc.URI, cc.Usage, cc.Registration, cc.Password, cc.Cache, cc.capacity.Decorator())
return NewLgEss(cc.URI, cc.Usage, cc.Registration, cc.Password, cc.Cache, cc.capacity.Decorator(), essType)
}

// NewLgEss creates an LgEss Meter
func NewLgEss(uri, usage, registration, password string, cache time.Duration, capacity func() float64) (api.Meter, error) {
lp, err := lgpcs.GetInstance(uri, registration, password, cache)
func NewLgEss(uri, usage, registration, password string, cache time.Duration, capacity func() float64, 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),
lp: lp,
conn: conn,
}

// decorate api.MeterEnergy
var totalEnergy func() (float64, error)
if m.usage == "grid" {
if m.usage == "grid" && essType != lgpcs.LgEss15 {
totalEnergy = m.totalEnergy
}

Expand All @@ -102,44 +115,44 @@ func NewLgEss(uri, usage, registration, password string, cache time.Duration, ca

// CurrentPower implements the api.Meter interface
func (m *LgEss) CurrentPower() (float64, error) {
data, err := m.lp.Data()
data, err := m.conn.Data()
if err != nil {
return 0, err
}

switch m.usage {
case "grid":
return data.GridPower, nil
return data.GetGridPower(), nil
case "pv":
return data.PvTotalPower, nil
return data.GetPvTotalPower(), nil
case "battery":
return data.BatConvPower, nil
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.lp.Data()
data, err := m.conn.Data()
if err != nil {
return 0, err
}

switch m.usage {
case "grid":
return data.CurrentGridFeedInEnergy / 1e3, nil
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.lp.Data()
data, err := m.conn.Data()
if err != nil {
return 0, err
}

return data.BatUserSoc, nil
return data.GetBatUserSoc(), nil
}
72 changes: 28 additions & 44 deletions meter/lgpcs/lgpcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,29 +23,13 @@ const (
MeterURI = "/v1/user/essinfo/home"
)

type MeterResponse struct {
Statistics EssData
Direction struct {
IsGridSelling int `json:"is_grid_selling_,string"`
IsBatteryDischarging int `json:"is_battery_discharging_,string"`
}
}

type EssData struct {
GridPower float64 `json:"grid_power,string"`
PvTotalPower float64 `json:"pcs_pv_total_power,string"`
BatConvPower float64 `json:"batconv_power,string"`
BatUserSoc float64 `json:"bat_user_soc,string"`
CurrentGridFeedInEnergy float64 `json:"current_grid_feed_in_energy,string"`
CurrentPvGenerationSum float64 `json:"current_pv_generation_sum,string"`
}

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)
}

Expand All @@ -55,7 +39,7 @@ var (
)

// GetInstance implements the singleton pattern to handle the access via the authkey to the PCS of the LG ESS HOME system
func GetInstance(uri, registration, password string, cache time.Duration) (*Com, error) {
func GetInstance(uri, registration, password string, cache time.Duration, essType Model) (*Com, error) {
uri = util.DefaultScheme(strings.TrimSuffix(uri, "/"), "https")

var err error
Expand All @@ -66,6 +50,7 @@ func GetInstance(uri, registration, password string, cache time.Duration) (*Com,
uri: uri,
authPath: UserLoginURI,
password: password,
essType: essType,
}

if registration != "" {
Expand Down Expand Up @@ -136,54 +121,53 @@ func (m *Com) Login() error {
return nil
}

// Data gives the data read from the pcs.
// Data gives the cached data read from the pcs.
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) {
if m.essType == LgEss8 {
var res MeterResponse8
err := m.update(&res)
return res, err
}

var res MeterResponse15
err := m.update(&res)
return res, err
}

func (m *Com) update(meterData any) error {
data := map[string]interface{}{
"auth_key": m.authKey,
}

req, err := request.New(http.MethodPost, m.uri+MeterURI, request.MarshalJSON(data), request.JSONEncoding)
if err != nil {
return EssData{}, err
return err
}

var resp MeterResponse

if err = m.DoJSON(req, &resp); err != nil {
if err := m.DoJSON(req, meterData); err != nil {
// re-login if request returns 405-error
var se request.StatusError
if errors.As(err, &se) && se.StatusCode() == http.StatusMethodNotAllowed {
err = m.Login()

if err == nil {
data["auth_key"] = m.authKey
req, err = request.New(http.MethodPost, m.uri+MeterURI, request.MarshalJSON(data), request.JSONEncoding)
if err := m.Login(); err != nil {
return err
}

if err == nil {
err = m.DoJSON(req, &resp)
data["auth_key"] = m.authKey
req, err := request.New(http.MethodPost, m.uri+MeterURI, request.MarshalJSON(data), request.JSONEncoding)
if err != nil {
return err
}
}
}

if err != nil {
return EssData{}, err
}

res := resp.Statistics
if resp.Direction.IsGridSelling > 0 {
res.GridPower = -res.GridPower
}
return m.DoJSON(req, &meterData)
}

// discharge battery: batPower is positive, charge battery: batPower is negative
if resp.Direction.IsBatteryDischarging == 0 {
res.BatConvPower = -res.BatConvPower
return err
}

return res, nil
return nil
}
116 changes: 116 additions & 0 deletions meter/lgpcs/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package lgpcs

import "math"

// Models
type Model int

const (
LgEss8 = 0 // lgess 8/10
LgEss15 = 1 // lgess 15
)

// data in the format expected by the accessing (lgess) module
type EssData interface {
GetGridPower() float64 // in [W]
GetPvTotalPower() float64 // in [W]
GetBatConvPower() float64 // in [W]
GetBatUserSoc() float64 // in [%]
GetCurrentGridFeedInEnergy() float64 // in [Wh]
GetCurrentPvGenerationSum() float64 // in [Wh]
}

type EssData8 struct {
GridPower float64 `json:"grid_power,string"`
PvTotalPower float64 `json:"pcs_pv_total_power,string"`
BatConvPower float64 `json:"batconv_power,string"`
BatUserSoc float64 `json:"bat_user_soc,string"`
CurrentGridFeedInEnergy float64 `json:"current_grid_feed_in_energy,string"`
CurrentPvGenerationSum float64 `json:"current_pv_generation_sum,string"`
}

type MeterResponse8 struct {
Statistics EssData8
Direction struct {
IsGridSelling int `json:"is_grid_selling_,string"`
IsBatteryDischarging int `json:"is_battery_discharging_,string"`
}
}

func (m MeterResponse8) GetGridPower() float64 {
if m.Direction.IsGridSelling > 0 {
return -m.Statistics.GridPower
}
return m.Statistics.GridPower
}

func (m MeterResponse8) GetPvTotalPower() float64 {
return m.Statistics.PvTotalPower
}

func (m MeterResponse8) GetBatConvPower() float64 {
// discharge battery: batPower is positive, charge battery: batPower is negative
if m.Direction.IsBatteryDischarging == 0 {
return -m.Statistics.BatConvPower
}
return m.Statistics.BatConvPower
}

func (m MeterResponse8) GetBatUserSoc() float64 {
return m.Statistics.BatUserSoc
}

func (m MeterResponse8) GetCurrentGridFeedInEnergy() float64 {
return m.Statistics.CurrentGridFeedInEnergy
}

func (m MeterResponse8) GetCurrentPvGenerationSum() float64 {
return m.Statistics.CurrentPvGenerationSum
}

// power values are in 100W units
type EssData15 struct {
GridPower int `json:"grid_power_01kW"`
PvTotalPower int `json:"pv_total_power_01kW"`
BatConvPower int `json:"batt_conv_power_01kW"`
BatUserSoc int `json:"bat_user_soc"`
}

type MeterResponse15 struct {
Statistics EssData15
Direction struct {
IsGridSelling int `json:"is_grid_selling_"`
IsBatteryDischarging int `json:"is_battery_discharging_"`
}
}

func (m MeterResponse15) GetGridPower() float64 {
if m.Direction.IsGridSelling > 0 {
return -float64(m.Statistics.GridPower * 100)
}
return float64(m.Statistics.GridPower * 100)
}

func (m MeterResponse15) GetPvTotalPower() float64 {
return float64(m.Statistics.PvTotalPower * 100)
}

func (m MeterResponse15) GetBatConvPower() float64 {
// discharge battery: batPower is positive, charge battery: batPower is negative
if m.Direction.IsBatteryDischarging == 0 {
return -float64(m.Statistics.BatConvPower * 100)
}
return float64(m.Statistics.BatConvPower * 100)
}

func (m MeterResponse15) GetBatUserSoc() float64 {
return float64(m.Statistics.BatUserSoc)
}

func (m MeterResponse15) GetCurrentGridFeedInEnergy() float64 {
return math.NaN() // data not provided by Ess15
}

func (m MeterResponse15) GetCurrentPvGenerationSum() float64 {
return math.NaN() // data not provided by Ess15
}
Loading

0 comments on commit f6c9a74

Please sign in to comment.