forked from evcc-io/evcc
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlgpcs.go
175 lines (142 loc) Β· 4.66 KB
/
lgpcs.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
// 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
}