diff --git a/meter/lgess.go b/meter/lgess.go index 4d0b788d32..6acc66d65f 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 ** @@ -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 @@ -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 } diff --git a/meter/lgpcs/lgpcs.go b/meter/lgpcs/lgpcs.go index 8342987f8e..0421093c91 100644 --- a/meter/lgpcs/lgpcs.go +++ b/meter/lgpcs/lgpcs.go @@ -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 ( @@ -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 @@ -49,6 +50,7 @@ func GetInstance(uri, registration, password string, cache time.Duration) (*Com, uri: uri, authPath: UserLoginURI, password: password, + essType: essType, } if registration != "" { @@ -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 { 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 + 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 + } } diff --git a/meter/lgpcs/types.go b/meter/lgpcs/types.go index 29c17af242..bdd25efb97 100644 --- a/meter/lgpcs/types.go +++ b/meter/lgpcs/types.go @@ -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 { @@ -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"` 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 0000000000..c94f8eed2e --- /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 10f05dfb09..39a8afbb2f 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 }}