From 85bb5283a2f9e23d95f1e6084aee234f353e535a Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 29 Nov 2024 10:33:17 +0100 Subject: [PATCH] Add LG ESS Home 15 (#17484) --- meter/lgess.go | 53 +++++--- meter/lgpcs/lgpcs.go | 74 +++++------ meter/lgpcs/types.go | 116 ++++++++++++++++++ .../definition/meter/lg-ess-home-15.yaml | 38 ++++++ .../definition/meter/lg-ess-home-8-10.yaml | 2 +- 5 files changed, 217 insertions(+), 66 deletions(-) create mode 100644 meter/lgpcs/types.go create mode 100644 templates/definition/meter/lg-ess-home-15.yaml diff --git a/meter/lgess.go b/meter/lgess.go index 2570cf989..72b978fc9 100644 --- a/meter/lgess.go +++ b/meter/lgess.go @@ -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: @@ -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 ** @@ -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 @@ -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 } @@ -102,18 +115,18 @@ 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) } @@ -121,14 +134,14 @@ func (m *LgEss) CurrentPower() (float64, error) { // 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) } @@ -136,10 +149,10 @@ func (m *LgEss) totalEnergy() (float64, error) { // 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 } diff --git a/meter/lgpcs/lgpcs.go b/meter/lgpcs/lgpcs.go index e06a1c35d..2fa571f62 100644 --- a/meter/lgpcs/lgpcs.go +++ b/meter/lgpcs/lgpcs.go @@ -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) } @@ -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 @@ -66,6 +50,7 @@ func GetInstance(uri, registration, password string, cache time.Duration) (*Com, uri: uri, authPath: UserLoginURI, password: password, + essType: essType, } if registration != "" { @@ -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 } + + return m.DoJSON(req, &meterData) } + + return err } - if err != nil { - return EssData{}, err - } - - res := resp.Statistics - if resp.Direction.IsGridSelling > 0 { - res.GridPower = -res.GridPower - } - - // discharge battery: batPower is positive, charge battery: batPower is negative - if resp.Direction.IsBatteryDischarging == 0 { - res.BatConvPower = -res.BatConvPower - } - - return res, nil + return nil } diff --git a/meter/lgpcs/types.go b/meter/lgpcs/types.go new file mode 100644 index 000000000..a6e440832 --- /dev/null +++ b/meter/lgpcs/types.go @@ -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 +} diff --git a/templates/definition/meter/lg-ess-home-15.yaml b/templates/definition/meter/lg-ess-home-15.yaml new file mode 100644 index 000000000..c94f8eed2 --- /dev/null +++ b/templates/definition/meter/lg-ess-home-15.yaml @@ -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 diff --git a/templates/definition/meter/lg-ess-home-8-10.yaml b/templates/definition/meter/lg-ess-home-8-10.yaml index 10f05dfb0..39a8afbb2 100644 --- a/templates/definition/meter/lg-ess-home-8-10.yaml +++ b/templates/definition/meter/lg-ess-home-8-10.yaml @@ -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 }}