Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added lgess15 support #17432

Open
wants to merge 1 commit into
base: feat/lgess15
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
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
98 changes: 73 additions & 25 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,53 +122,99 @@ 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) {
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about

func (m *Com) refresh(res any) error 

and then call it with

var res EssData8
err := m.refresh(&res)

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 {
err = m.Login()
if err = m.Login(); err != nil {
return err
}

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

if err == nil {
err = m.DoJSON(req, &resp)
if err = m.DoJSON(req, meterResponse); err != nil {
return err
}
}
}

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a bit awkward. An alternative would be to make EssData8 and EssData15 implement a common interface?

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