Add LG ESS Home 15 (#17484)
This commit is contained in:
parent
d75f79f76e
commit
85bb5283a2
5 changed files with 217 additions and 66 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
116
meter/lgpcs/types.go
Normal 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
|
||||
}
|
||||
38
templates/definition/meter/lg-ess-home-15.yaml
Normal file
38
templates/definition/meter/lg-ess-home-15.yaml
Normal 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
|
||||
|
|
@ -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 }}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue