Skip to content

Commit

Permalink
added lgess15 support
Browse files Browse the repository at this point in the history
  • Loading branch information
texperiri committed Nov 24, 2024
1 parent 13b300c commit 1ca7f77
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 30 deletions.
31 changes: 22 additions & 9 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 @@ -46,13 +50,22 @@ type LgEss struct {
}

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.LgEssType) (api.Meter, error) {
cc := struct {
capacity `mapstructure:",squash"`
URI, Usage string
Expand All @@ -70,12 +83,12 @@ 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) {
conn, err := lgpcs.GetInstance(uri, registration, password, cache)
func NewLgEss(uri, usage, registration, password string, cache time.Duration, capacity func() float64, essType lgpcs.LgEssType) (api.Meter, error) {
conn, err := lgpcs.GetInstance(uri, registration, password, cache, essType)
if err != nil {
return nil, err
}
Expand Down
88 changes: 68 additions & 20 deletions meter/lgpcs/lgpcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ 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
dataG func() (EssData8, error)
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 LgEssType // currently the LG Ess Home 8/10 and Home 15 are supported
dataG func() (EssData, error)
}

var (
Expand All @@ -38,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 LgEssType) (*Com, error) {
uri = util.DefaultScheme(strings.TrimSuffix(uri, "/"), "https")

var err error
Expand All @@ -49,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 @@ -120,24 +122,45 @@ func (m *Com) Login() error {
}

// Data gives the data read from the pcs.
func (m *Com) Data() (EssData8, error) {
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() (EssData8, error) {
func (m *Com) refreshData() (EssData, error) {

Check failure on line 130 in meter/lgpcs/lgpcs.go

View workflow job for this annotation

GitHub Actions / Lint

unnecessary leading newline (whitespace)

var data EssData
var err error
if m.essType == LgEss8 {
var meterResponse MeterResponse8
if err = refreshDataFromLgess[MeterResponse8](&meterResponse, m); err != nil {
return EssData{}, err
}
essDataFromMeterResponse8(&data, &meterResponse)
} else {
var meterResponse MeterResponse15
if err = refreshDataFromLgess[MeterResponse15](&meterResponse, m); err != nil {
return EssData{}, err
}
essDataFromMeterResponse15(&data, &meterResponse)
}

return data, nil
}

// read data from Lgess, reconnects if session expired
func refreshDataFromLgess[T MeterResponse8 | MeterResponse15](meterResponse *T, m *Com) error {

Check failure on line 152 in meter/lgpcs/lgpcs.go

View workflow job for this annotation

GitHub Actions / Lint

unnecessary leading newline (whitespace)

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 EssData8{}, err
return err
}

var resp MeterResponse8

if err = m.DoJSON(req, &resp); err != nil {
if err := m.DoJSON(req, meterResponse); err != nil {
// re-login if request returns 405-error
var se request.StatusError
if errors.As(err, &se) && se.StatusCode() == http.StatusMethodNotAllowed {
Expand All @@ -149,24 +172,49 @@ func (m *Com) refreshData() (EssData8, error) {
}

if err == nil {
err = m.DoJSON(req, &resp)
err = m.DoJSON(req, meterResponse)

Check failure on line 175 in meter/lgpcs/lgpcs.go

View workflow job for this annotation

GitHub Actions / Lint

ineffectual assignment to err (ineffassign)
}
}
}

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

res := resp.Statistics
if resp.Direction.IsGridSelling > 0 {
res.GridPower = -res.GridPower
// convert response from LgEss8/10 into interface EssData
func essDataFromMeterResponse8(essData *EssData, meterResponse *MeterResponse8) {
essData.GridPower = meterResponse.Statistics.GridPower
essData.PvTotalPower = meterResponse.Statistics.PvTotalPower
essData.BatConvPower = meterResponse.Statistics.BatConvPower
essData.BatUserSoc = meterResponse.Statistics.BatUserSoc
essData.CurrentGridFeedInEnergy = meterResponse.Statistics.CurrentGridFeedInEnergy
essData.CurrentPvGenerationSum = meterResponse.Statistics.CurrentPvGenerationSum

if meterResponse.Direction.IsGridSelling > 0 {
essData.GridPower = -essData.GridPower
}

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

return res, nil
// convert response from LgEss15 into interface EssData
func essDataFromMeterResponse15(essData *EssData, meterResponse *MeterResponse15) {
// Ess15 meter gives data in 100W units.
essData.GridPower = float64(meterResponse.Statistics.GridPower * 100)
essData.PvTotalPower = float64(meterResponse.Statistics.PvTotalPower * 100)
essData.BatConvPower = float64(meterResponse.Statistics.BatConvPower * 100)
essData.BatUserSoc = float64(meterResponse.Statistics.BatUserSoc * 100)
essData.CurrentGridFeedInEnergy = float64(0) // data not provided by Ess15
essData.CurrentPvGenerationSum = float64(0) // data not provided by Ess15

if meterResponse.Direction.IsGridSelling > 0 {
essData.GridPower = -essData.GridPower
}

// discharge battery: batPower is positive, charge battery: batPower is negative
if meterResponse.Direction.IsBatteryDischarging == 0 {
essData.BatConvPower = -essData.BatConvPower
}
}
18 changes: 18 additions & 0 deletions meter/lgpcs/types.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
package lgpcs

// LgEssTypes
type LgEssType int64

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

type MeterResponse8 struct {
Statistics EssData8
Direction struct {
Expand All @@ -8,6 +16,16 @@ type MeterResponse8 struct {
}
}

// data in the format expected by the accessing (lgess) module
type EssData struct {
GridPower float64
PvTotalPower float64
BatConvPower float64
BatUserSoc float64
CurrentGridFeedInEnergy float64
CurrentPvGenerationSum float64
}

type EssData8 struct {
GridPower float64 `json:"grid_power,string"`
PvTotalPower float64 `json:"pcs_pv_total_power,string"`
Expand Down
38 changes: 38 additions & 0 deletions templates/definition/meter/lg-ess-home-15.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
template: lg-ess-home-15
products:
- brand: LG
description:
generic: ESS Home 15
params:
- name: usage
choice: ["grid", "pv", "battery"]
allinone: true
- name: host
- name: password
help:
en: >
User password, see https://github.com/Morluktom/ioBroker.lg-ess-home/tree/master#getting-the-password.
Alteratively, use registration id for admin login.
de: >
Benutzerpasswort, siehe https://github.com/Morluktom/ioBroker.lg-ess-home/tree/master#getting-the-password.
Alternativ kann die Registriernummer für Administratorlogin verwendet werden.
- name: registration
advanced: true
example: "DE200..."
help:
en: Registration id of the LG ESS HOME inverter.
de: Registriernummer des LG ESS HOME Wechselrichters.
- name: capacity
advanced: true
render: |
type: lgess15
usage: {{ .usage }}
# uri and password are only required once if multiple lgess usages are defined
uri: https://{{ .host }}
{{- if .password }}
password: {{ .password }}
{{- end }}
{{- if .registration }}
registration: {{ .registration }}
{{- end }}
capacity: {{ .capacity }} # kWh
2 changes: 1 addition & 1 deletion templates/definition/meter/lg-ess-home-8-10.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ params:
- name: capacity
advanced: true
render: |
type: lgess
type: lgess8
usage: {{ .usage }}
# uri and password are only required once if multiple lgess usages are defined
uri: https://{{ .host }}
Expand Down

0 comments on commit 1ca7f77

Please sign in to comment.