evcc-io/meter/lgpcs/lgpcs.go

175 lines
4.7 KiB
Go

// Package lgpcs implements access to the LG pcs device (aka inverter).
// Pcs is the LG power conditioning system that converts the PV (or battery) - DC current into AC current (and controls the batteries)
package lgpcs
import (
"errors"
"fmt"
"net/http"
"strings"
"sync"
"time"
"github.com/evcc-io/evcc/provider"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"github.com/evcc-io/evcc/util/transport"
)
// URIs
const (
LoginURI = "/v1/login"
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"
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() (EssData, error)
}
var (
once sync.Once
instance *Com
)
// GetInstance implements the singleton pattern to handle the access via the authkey to the PCS of the LG ESS HOME system
func GetInstance(uri, password string, cache time.Duration) (*Com, error) {
uri = util.DefaultScheme(strings.TrimSuffix(uri, "/"), "https")
var err error
once.Do(func() {
log := util.NewLogger("lgess")
instance = &Com{
Helper: request.NewHelper(log),
uri: uri,
password: password,
}
// ignore the self signed certificate
instance.Client.Transport = request.NewTripper(log, transport.Insecure())
// caches the data access for the "cache" time duration
// sends a new request to the pcs if the cache is expired and Data() requested
instance.dataG = provider.Cached(instance.refreshData, cache)
// do first login if no authKey exists and uri and password exist
if instance.authKey == "" && instance.uri != "" && instance.password != "" {
err = instance.Login()
}
})
// check if different uris are provided
if uri != "" && instance.uri != uri {
return nil, fmt.Errorf("uri mismatch: %s vs %s", instance.uri, uri)
}
// check if different passwords are provided
if password != "" && instance.password != password {
return nil, errors.New("password mismatch")
}
return instance, err
}
// Login calls login and stores the returned authorization key
func (m *Com) Login() error {
data := map[string]interface{}{
"password": m.password,
}
req, err := request.New(http.MethodPut, m.uri+LoginURI, request.MarshalJSON(data), request.JSONEncoding)
if err != nil {
return err
}
// read auth_key from response body
var res struct {
Status string `json:"status,omitempty"`
AuthKey string `json:"auth_key"`
}
if err := m.DoJSON(req, &res); err != nil {
return fmt.Errorf("login failed: %w", err)
}
// check login response status
if res.Status != "success" {
return fmt.Errorf("login failed: %s", res.Status)
}
// read auth_key from response
m.authKey = res.AuthKey
return nil
}
// Data gives the 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) {
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
}
var resp MeterResponse
if err = m.DoJSON(req, &resp); err != nil {
// re-login if request returns 405-error
if err2, ok := err.(request.StatusError); ok && err2.HasStatus(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 == nil {
err = m.DoJSON(req, &resp)
}
}
}
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
}