Skip to content

Commit dc9aeae

Browse files
authored
Merge pull request #1 from andig/master
Trying to rebase (?) Alestrix/evcc:master to andig/evcc:master
2 parents f15daee + 2f6f48b commit dc9aeae

File tree

8 files changed

+542
-45
lines changed

8 files changed

+542
-45
lines changed

detect/definitions.go

+10
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const (
3737
taskMeter = "meter"
3838
taskFronius = "fronius"
3939
taskTasmota = "tasmota"
40+
taskTPLink = "tplink"
4041
)
4142

4243
func init() {
@@ -241,6 +242,15 @@ func init() {
241242
},
242243
})
243244

245+
taskList.Add(Task{
246+
ID: taskTPLink,
247+
Type: "tcp",
248+
Depends: TaskPing,
249+
Config: map[string]interface{}{
250+
"port": 9999, // TP-Link Smart Home Protocol standard port
251+
},
252+
})
253+
244254
// taskList.Add(Task{
245255
// ID: "volkszähler",
246256
// Type: "http",

internal/charger/tplink.go

+191
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package charger
2+
3+
import (
4+
"bytes"
5+
"encoding/binary"
6+
"encoding/json"
7+
"errors"
8+
"fmt"
9+
"net"
10+
"strings"
11+
"time"
12+
13+
"github.com/andig/evcc/api"
14+
"github.com/andig/evcc/internal/charger/tplink"
15+
"github.com/andig/evcc/util"
16+
)
17+
18+
// TPLink charger implementation
19+
type TPLink struct {
20+
uri string
21+
standbypower float64
22+
}
23+
24+
func init() {
25+
registry.Add("tplink", NewTPLinkFromConfig)
26+
}
27+
28+
// NewTPLinkFromConfig creates a TP-Link charger from generic config
29+
func NewTPLinkFromConfig(other map[string]interface{}) (api.Charger, error) {
30+
cc := struct {
31+
URI string
32+
StandbyPower float64
33+
}{}
34+
35+
if err := util.DecodeOther(other, &cc); err != nil {
36+
return nil, err
37+
}
38+
39+
if cc.URI == "" {
40+
return nil, errors.New("missing uri")
41+
}
42+
43+
return NewTPLink(cc.URI, cc.StandbyPower)
44+
}
45+
46+
// NewTPLink creates TP-Link charger
47+
func NewTPLink(uri string, standbypower float64) (*TPLink, error) {
48+
c := &TPLink{
49+
uri: net.JoinHostPort(uri, "9999"),
50+
standbypower: standbypower,
51+
}
52+
return c, nil
53+
}
54+
55+
// Enabled implements the Charger.Enabled interface
56+
func (c *TPLink) Enabled() (bool, error) {
57+
sysResp, err := c.execCmd(`{"system":{"get_sysinfo":null}}`)
58+
if err != nil {
59+
return false, err
60+
}
61+
62+
var systemResponse tplink.SystemResponse
63+
if err := json.Unmarshal(sysResp, &systemResponse); err != nil {
64+
return false, err
65+
}
66+
67+
if err := systemResponse.System.GetSysinfo.ErrCode; err != 0 {
68+
return false, fmt.Errorf("get_sysinfo error %d", err)
69+
}
70+
71+
if !strings.Contains(systemResponse.System.GetSysinfo.Feature, "ENE") {
72+
return false, errors.New(systemResponse.System.GetSysinfo.Model + " not supported, energy meter feature missing")
73+
}
74+
75+
return int(1) == systemResponse.System.GetSysinfo.RelayState, err
76+
}
77+
78+
// Enable implements the Charger.Enable interface
79+
func (c *TPLink) Enable(enable bool) error {
80+
cmd := `{"system":{"set_relay_state":{"state":0}}}`
81+
if enable {
82+
cmd = `{"system":{"set_relay_state":{"state":1}}}`
83+
}
84+
85+
// Execute TP-Link set_relay_state command
86+
sysResp, err := c.execCmd(cmd)
87+
if err != nil {
88+
return err
89+
}
90+
91+
var systemResponse tplink.SystemResponse
92+
if err := json.Unmarshal(sysResp, &systemResponse); err != nil {
93+
return err
94+
}
95+
96+
if err := systemResponse.System.SetRelayState.ErrCode; err != 0 {
97+
return fmt.Errorf("set_relay_state error %d", err)
98+
}
99+
100+
return nil
101+
}
102+
103+
// MaxCurrent implements the Charger.MaxCurrent interface
104+
func (c *TPLink) MaxCurrent(current int64) error {
105+
return nil
106+
}
107+
108+
// Status implements the Charger.Status interface
109+
func (c *TPLink) Status() (api.ChargeStatus, error) {
110+
power, err := c.CurrentPower()
111+
112+
switch {
113+
case power > 0:
114+
return api.StatusC, err
115+
default:
116+
return api.StatusB, err
117+
}
118+
}
119+
120+
var _ api.Meter = (*TPLink)(nil)
121+
122+
// CurrentPower implements the api.Meter interface
123+
func (c *TPLink) CurrentPower() (float64, error) {
124+
emeResp, err := c.execCmd(`{"emeter":{"get_realtime":null}}`)
125+
if err != nil {
126+
return 0, err
127+
}
128+
129+
var emeterResponse tplink.EmeterResponse
130+
if err := json.Unmarshal(emeResp, &emeterResponse); err != nil {
131+
return 0, err
132+
}
133+
if err := emeterResponse.Emeter.GetRealtime.ErrCode; err != 0 {
134+
return 0, fmt.Errorf("get_realtime error %d", err)
135+
}
136+
137+
power := emeterResponse.Emeter.GetRealtime.PowerMw / 1000
138+
if power == 0 {
139+
power = emeterResponse.Emeter.GetRealtime.Power
140+
}
141+
142+
// ignore standby power
143+
if power < c.standbypower {
144+
power = 0
145+
}
146+
147+
return power, err
148+
}
149+
150+
// execCmd executes an TP-Link Smart Home Protocol command and provides the response
151+
func (c *TPLink) execCmd(cmd string) ([]byte, error) {
152+
// encode command message
153+
buf := bytes.NewBuffer([]byte{0, 0, 0, 0})
154+
var ekey byte = 171 // initialization vector
155+
for i := 0; i < len(cmd); i++ {
156+
ekey = ekey ^ cmd[i]
157+
_ = buf.WriteByte(ekey)
158+
}
159+
160+
// write 4 bytes to start of buffer with command length
161+
binary.BigEndian.PutUint32(buf.Bytes(), uint32(buf.Len()-4))
162+
163+
// open connection via TP-Link Smart Home Protocol
164+
conn, err := net.DialTimeout("tcp", c.uri, 5*time.Second)
165+
if err != nil {
166+
return nil, err
167+
}
168+
defer conn.Close()
169+
170+
// send command
171+
if _, err = buf.WriteTo(conn); err != nil {
172+
return nil, err
173+
}
174+
175+
// read response
176+
resp := make([]byte, 2048)
177+
n, err := conn.Read(resp)
178+
if err != nil {
179+
return nil, err
180+
}
181+
182+
// decode response message
183+
var dkey byte = 171 // initialization vector
184+
for i := 4; i < n; i++ {
185+
dec := dkey ^ resp[i]
186+
dkey = resp[i]
187+
_ = buf.WriteByte(dec)
188+
}
189+
190+
return buf.Bytes(), nil
191+
}

internal/charger/tplink/types.go

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package tplink
2+
3+
// TP-Link smart power plug/outlet responses
4+
// https://www.softscheck.com/en/reverse-engineering-tp-link-hs110/#Portscan
5+
6+
// SystemResponse is the TP-Link plug/outlet api system response
7+
type SystemResponse struct {
8+
System struct {
9+
SetRelayState struct {
10+
ErrCode int `json:"err_code,omitempty"`
11+
} `json:"set_relay_state"`
12+
GetSysinfo struct {
13+
ErrCode int `json:"err_code,omitempty"`
14+
SwVer string `json:"sw_ver,omitempty"`
15+
Model string `json:"model,omitempty"`
16+
Alias string `json:"alias,omitempty"`
17+
DevName string `json:"dev_name,omitempty"`
18+
RelayState int `json:"relay_state,omitempty"`
19+
Feature string `json:"feature,omitempty"`
20+
} `json:"get_sysinfo"`
21+
} `json:"system"`
22+
}
23+
24+
// EmeterResponse is the TP-Link plug/outlet api emeter response
25+
type EmeterResponse struct {
26+
Emeter struct {
27+
GetRealtime struct {
28+
// 1st plug generation E-Meter Response
29+
Current float64 `json:"current,omitempty"`
30+
Voltage float64 `json:"voltage,omitempty"`
31+
Power float64 `json:"power,omitempty"`
32+
Total float64 `json:"total,omitempty"`
33+
// 2nd plug generation E-Meter Response
34+
CurrentMa float64 `json:"current_ma,omitempty"`
35+
VoltageMv float64 `json:"voltage_mv,omitempty"`
36+
PowerMw float64 `json:"power_mw,omitempty"`
37+
TotalWh float64 `json:"total_wh,omitempty"`
38+
ErrCode int `json:"err_code,omitempty"`
39+
} `json:"get_realtime"`
40+
} `json:"emeter"`
41+
}

internal/charger/tplink/types_test.go

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package tplink
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
)
7+
8+
func TestUnmarshalTPLinkSystemResponses(t *testing.T) {
9+
var sysresp SystemResponse
10+
11+
// Test set_relay_state response
12+
jsonstr := `{"system":{"set_relay_state":{"err_code":0}}}`
13+
if err := json.Unmarshal([]byte(jsonstr), &sysresp); err != nil {
14+
t.Error(err)
15+
}
16+
if sysresp.System.SetRelayState.ErrCode != 0 {
17+
t.Error("SetRelayState.ErrCode")
18+
}
19+
20+
// Test get_sysinfo response
21+
jsonstr = `{"system":{"get_sysinfo":{"err_code":0,"sw_ver":"1.2.6 Build 200727 Rel.120821","hw_ver":"1.0","type":"IOT.SMARTPLUGSWITCH","model":"HS110(EU)","mac":"50:C7:BF:42:60:9B","deviceId":"80068B6B73AAD8C4A4D0F7B9AB5F8B1B1838EEAC","hwId":"45E29DA8382494D2E82688B52A0B2EB5","fwId":"00000000000000000000000000000000","oemId":"3D341ECE302C0642C99E31CE2430544B","alias":"evcc-charger","dev_name":"Wi-Fi Smart Plug With Energy Monitoring","icon_hash":"","relay_state":1,"on_time":110,"active_mode":"schedule","feature":"TIM:ENE","updating":0,"rssi":-54,"led_off":0,"latitude":49.817090,"longitude":9.056194}}}`
22+
if err := json.Unmarshal([]byte(jsonstr), &sysresp); err != nil {
23+
t.Error(err)
24+
}
25+
if sysresp.System.GetSysinfo.ErrCode != 0 {
26+
t.Error("GetSysinfo.ErrCode")
27+
}
28+
if sysresp.System.GetSysinfo.SwVer != "1.2.6 Build 200727 Rel.120821" {
29+
t.Error("GetSysinfo.SwVer")
30+
}
31+
if sysresp.System.GetSysinfo.Model != "HS110(EU)" {
32+
t.Error("GetSysinfo.Model")
33+
}
34+
if sysresp.System.GetSysinfo.Alias != "evcc-charger" {
35+
t.Error("GetSysinfo.Alias")
36+
}
37+
if sysresp.System.GetSysinfo.RelayState != 1 {
38+
t.Error("GetSysinfo.RelayState")
39+
}
40+
if sysresp.System.GetSysinfo.Feature != "TIM:ENE" {
41+
t.Error("GetSysinfo.Feature")
42+
}
43+
44+
// Test 1st emeter generation response
45+
var emeresp EmeterResponse
46+
jsonstr = `{"emeter":{"get_realtime":{"current":0.033759,"voltage":234.824322,"power":3.121391,"total":0.015000,"err_code":0}}}`
47+
if err := json.Unmarshal([]byte(jsonstr), &emeresp); err != nil {
48+
t.Error(err)
49+
}
50+
if emeresp.Emeter.GetRealtime.Current != 0.033759 {
51+
t.Error("GetRealtime.Current")
52+
}
53+
if emeresp.Emeter.GetRealtime.Voltage != 234.824322 {
54+
t.Error("GetRealtime.Voltage")
55+
}
56+
if emeresp.Emeter.GetRealtime.Power != 3.121391 {
57+
t.Error("GetRealtime.Power")
58+
}
59+
if emeresp.Emeter.GetRealtime.Total != 0.015000 {
60+
t.Error("GetRealtime.Total")
61+
}
62+
if emeresp.Emeter.GetRealtime.ErrCode != 0 {
63+
t.Error("GetRealtime.ErrCode")
64+
}
65+
66+
// Test 2nd emeter generation response
67+
var emeresp2 EmeterResponse
68+
jsonstr = ` {"emeter":{"get_realtime":{"voltage_mv":237119,"current_ma":218,"power_mw":31259,"total_wh":107,"err_code":0}}}`
69+
if err := json.Unmarshal([]byte(jsonstr), &emeresp2); err != nil {
70+
t.Error(err)
71+
}
72+
if emeresp2.Emeter.GetRealtime.CurrentMa != 218 {
73+
t.Error("GetRealtime.CurrentMa")
74+
}
75+
if emeresp2.Emeter.GetRealtime.VoltageMv != 237119 {
76+
t.Error("GetRealtime.VoltageMv")
77+
}
78+
if emeresp2.Emeter.GetRealtime.PowerMw != 31259 {
79+
t.Error("GetRealtime.PowerMw")
80+
}
81+
if emeresp2.Emeter.GetRealtime.TotalWh != 107 {
82+
t.Error("GetRealtime.TotalWh")
83+
}
84+
if emeresp2.Emeter.GetRealtime.ErrCode != 0 {
85+
t.Error("GetRealtime.ErrCode")
86+
}
87+
88+
}

0 commit comments

Comments
 (0)