forked from evcc-io/evcc
-
Notifications
You must be signed in to change notification settings - Fork 0
/
shelly.go
234 lines (193 loc) Β· 5.53 KB
/
shelly.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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
package charger
import (
"errors"
"fmt"
"net/http"
"strings"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/charger/shelly"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"github.com/evcc-io/evcc/util/transport"
"github.com/jpfielding/go-http-digest/pkg/digest"
)
// Shelly charger implementation
type Shelly struct {
*request.Helper
log *util.Logger
uri string
gen int // Shelly api generation
channel int
standbypower float64
}
func init() {
registry.Add("shelly", NewShellyFromConfig)
}
// NewShellyFromConfig creates a Shelly charger from generic config
func NewShellyFromConfig(other map[string]interface{}) (api.Charger, error) {
cc := struct {
URI string
User string
Password string
Channel int
StandbyPower float64
}{}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
if cc.URI == "" {
return nil, errors.New("missing uri")
}
return NewShelly(cc.URI, cc.User, cc.Password, cc.Channel, cc.StandbyPower)
}
// NewShelly creates Shelly charger
func NewShelly(uri, user, password string, channel int, standbypower float64) (*Shelly, error) {
for _, suffix := range []string{"/", "/rcp", "/shelly"} {
uri = strings.TrimSuffix(uri, suffix)
}
log := util.NewLogger("shelly")
client := request.NewHelper(log)
// Shelly Gen1 and Gen2 families expose the /shelly endpoint
var resp shelly.DeviceInfo
if err := client.GetJSON(fmt.Sprintf("%s/shelly", util.DefaultScheme(uri, "http")), &resp); err != nil {
return nil, err
}
c := &Shelly{
Helper: client,
log: log,
channel: channel,
standbypower: standbypower,
gen: resp.Gen,
}
c.Client.Transport = request.NewTripper(log, transport.Insecure())
if (resp.Auth || resp.AuthEn) && (user == "" || password == "") {
return c, fmt.Errorf("%s (%s) missing user/password", resp.Model, resp.Mac)
}
switch c.gen {
case 0, 1:
// Shelly GEN 1 API
// https://shelly-api-docs.shelly.cloud/gen1/#shelly-family-overview
c.uri = util.DefaultScheme(uri, "http")
if user != "" {
log.Redact(transport.BasicAuthHeader(user, password))
c.Client.Transport = transport.BasicAuth(user, password, c.Client.Transport)
}
if resp.NumMeters == 0 {
return c, fmt.Errorf("%s (%s) gen1 missing power meter ", resp.Model, resp.Mac)
}
case 2:
// Shelly GEN 2 API
// https://shelly-api-docs.shelly.cloud/gen2/
c.uri = fmt.Sprintf("%s/rpc", util.DefaultScheme(uri, "http"))
if user != "" {
c.Client.Transport = digest.NewTransport(user, password, c.Client.Transport)
}
default:
return c, fmt.Errorf("%s (%s) unknown api generation (%d)", resp.Type, resp.Model, c.gen)
}
return c, nil
}
// Enabled implements the api.Charger interface
func (c *Shelly) Enabled() (bool, error) {
switch c.gen {
case 0, 1:
var resp shelly.Gen1SwitchResponse
uri := fmt.Sprintf("%s/relay/%d", c.uri, c.channel)
err := c.GetJSON(uri, &resp)
return resp.Ison, err
default:
var resp shelly.Gen2SwitchResponse
err := c.execGen2Cmd("Switch.GetStatus", false, &resp)
return resp.Output, err
}
}
// Enable implements the api.Charger interface
func (c *Shelly) Enable(enable bool) error {
var err error
onoff := map[bool]string{true: "on", false: "off"}
switch c.gen {
case 0, 1:
var resp shelly.Gen1SwitchResponse
uri := fmt.Sprintf("%s/relay/%d?turn=%s", c.uri, c.channel, onoff[enable])
err = c.GetJSON(uri, &resp)
default:
var resp shelly.Gen2SwitchResponse
err = c.execGen2Cmd("Switch.Set", enable, &resp)
}
if err != nil {
return err
}
enabled, err := c.Enabled()
switch {
case err != nil:
return err
case enable != enabled:
return fmt.Errorf("switch %s failed", onoff[enable])
default:
return nil
}
}
// MaxCurrent implements the api.Charger interface
func (c *Shelly) MaxCurrent(current int64) error {
return nil
}
// Status implements the api.Charger interface
func (c *Shelly) Status() (api.ChargeStatus, error) {
power, err := c.CurrentPower()
if power > c.standbypower {
return api.StatusC, err
}
return api.StatusB, err
}
var _ api.Meter = (*Shelly)(nil)
// CurrentPower implements the api.Meter interface
func (c *Shelly) CurrentPower() (float64, error) {
var power float64
switch c.gen {
case 0, 1:
var resp shelly.Gen1StatusResponse
uri := fmt.Sprintf("%s/status", c.uri)
if err := c.GetJSON(uri, &resp); err != nil {
return 0, err
}
if c.channel >= len(resp.Meters) {
return 0, errors.New("invalid channel, missing power meter")
}
power = resp.Meters[c.channel].Power
default:
var resp shelly.Gen2StatusResponse
if err := c.execGen2Cmd("Shelly.GetStatus", false, &resp); err != nil {
return 0, err
}
switch c.channel {
case 1:
power = resp.Switch1.Apower
case 2:
power = resp.Switch2.Apower
default:
power = resp.Switch0.Apower
}
}
// ignore standby power
if power <= c.standbypower {
power = 0
}
return power, nil
}
// execGen2Cmd executes a shelly api gen1/gen2 command and provides the response
func (c *Shelly) execGen2Cmd(method string, enable bool, res interface{}) error {
// Shelly gen 2 rfc7616 authentication
// https://shelly-api-docs.shelly.cloud/gen2/Overview/CommonDeviceTraits#authentication
// https://datatracker.ietf.org/doc/html/rfc7616
data := &shelly.Gen2RpcPost{
Id: c.channel,
On: enable,
Src: "evcc",
Method: method,
}
req, err := request.New(http.MethodPost, fmt.Sprintf("%s/%s", c.uri, method), request.MarshalJSON(data), request.JSONEncoding)
if err != nil {
return err
}
return c.DoJSON(req, &res)
}