Add LG ESS Home 15 (#17484)

This commit is contained in:
andig 2024-11-29 10:33:17 +01:00 • committed by GitHub
parent d75f79f76e
commit 85bb5283a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 217 additions and 66 deletions

View file

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

View file

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

116
meter/lgpcs/types.go Normal file
View file

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

View file

@ -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

View file

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