WIPing intensifies
This commit is contained in:
parent
6eda162a70
commit
558a53e965
2 changed files with 175 additions and 37 deletions
150
plug.go
150
plug.go
|
@ -1,38 +1,71 @@
|
||||||
// Package plugctl provides an API for Kasa smart home switches.
|
// Package plugctl provides an API for controlling Kasa smart home switches.
|
||||||
package plugctl
|
package plugctl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Plug provides an API for a single Kasa smart home switch.
|
const (
|
||||||
|
reqTurnOn = `{"system":{"set_relay_state":{"state":1}}}`
|
||||||
|
reqTurnOff = `{"system":{"set_relay_state":{"state":0}}}`
|
||||||
|
reqGetSysinfo = `{"system":{"get_sysinfo":null}}`
|
||||||
|
reqGetEMeter = `{"emeter":{"get_realtime":null}}`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Plug provides an API for a single Kasa smart home plug or switch.
|
||||||
type Plug struct {
|
type Plug struct {
|
||||||
Addr net.TCPAddr
|
Addr string
|
||||||
}
|
}
|
||||||
|
|
||||||
type msg struct {
|
func (p *Plug) roundTrip(req string) (*response, error) {
|
||||||
System *systemMsg `json:"system,omitempty"`
|
conn, err := net.Dial("tcp", p.Addr)
|
||||||
EMeter *eMeterMsg `json:"emeter,omitempty"`
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("dialing plug: %w", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
lp := []byte{0, 0, 0, 0}
|
||||||
|
binary.BigEndian.PutUint32(lp, uint32(len(req)))
|
||||||
|
|
||||||
|
// Start by writing length
|
||||||
|
if _, err := conn.Write(lp); err != nil {
|
||||||
|
return nil, fmt.Errorf("writing length: %w", err)
|
||||||
|
}
|
||||||
|
// Rest of the message is encrypted
|
||||||
|
enc := newEncrypter(conn)
|
||||||
|
if _, err := enc.Write([]byte(req)); err != nil {
|
||||||
|
return nil, fmt.Errorf("writing message: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
type eMeterMsg struct {
|
// Read length of response
|
||||||
GetRealtime *getRealtimeMsg `json:"get_realtime"`
|
if _, err := io.ReadFull(conn, lp); err != nil {
|
||||||
|
return nil, fmt.Errorf("reading length: %w", err)
|
||||||
|
}
|
||||||
|
rlen := binary.BigEndian.Uint32(lp)
|
||||||
|
|
||||||
|
// Read the encrypted response
|
||||||
|
buf := make([]byte, rlen)
|
||||||
|
if _, err := io.ReadFull(conn, buf); err != nil {
|
||||||
|
return nil, fmt.Errorf("reading response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
type getRealtimeMsg struct{}
|
// Decrypt and parse the response
|
||||||
|
var resp *response
|
||||||
type systemMsg struct {
|
dec := json.NewDecoder(newDecrypter(bytes.NewReader(buf)))
|
||||||
GetSysInfo *getSysInfoMsg `json:"get_sysinfo"`
|
if err := dec.Decode(&resp); err != nil {
|
||||||
SetRelayState *setRelayStateMsg `json:"set_relay_state,omitempty"`
|
return nil, fmt.Errorf("reading message: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
type getSysInfoMsg struct{}
|
return resp, nil
|
||||||
|
|
||||||
type setRelayStateMsg struct {
|
|
||||||
State int `json:"state"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implements TP Link's weaksauce encryption.
|
||||||
|
// Do NOT use for actually important secrets.
|
||||||
type decryptReader struct {
|
type decryptReader struct {
|
||||||
r io.Reader
|
r io.Reader
|
||||||
c byte
|
c byte
|
||||||
|
@ -54,6 +87,8 @@ func (d *decryptReader) Read(b []byte) (int, error) {
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implements TP Link's weaksauce encryption.
|
||||||
|
// Do NOT use for actually important secrets.
|
||||||
type encryptWriter struct {
|
type encryptWriter struct {
|
||||||
w io.Writer
|
w io.Writer
|
||||||
c byte
|
c byte
|
||||||
|
@ -74,3 +109,84 @@ func (e *encryptWriter) Write(b []byte) (int, error) {
|
||||||
}
|
}
|
||||||
return e.w.Write(eb)
|
return e.w.Write(eb)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
"sw_ver": "1.0.3 Build 210506 Rel.105435",
|
||||||
|
"hw_ver": "1.0",
|
||||||
|
"model": "KP105(AU)",
|
||||||
|
"deviceId": "8006CF0E89296D2A5BFBDE11C844BB2B1E2D2B7C",
|
||||||
|
"oemId": "EAB15315847573797920EFE7E9000562",
|
||||||
|
"hwId": "120EF751A1A4F170831E6C1F57D97B38",
|
||||||
|
"rssi": -58,
|
||||||
|
"longitude_i": 1473005,
|
||||||
|
"latitude_i": -428429,
|
||||||
|
"alias": "Car charger",
|
||||||
|
"status": "new",
|
||||||
|
"mic_type": "IOT.SMARTPLUGSWITCH",
|
||||||
|
"feature": "TIM",
|
||||||
|
"mac": "00:5F:67:01:A5:50",
|
||||||
|
"updating": 0,
|
||||||
|
"led_off": 0,
|
||||||
|
"obd_src": "tplink",
|
||||||
|
"relay_state": 0,
|
||||||
|
"on_time": 0,
|
||||||
|
"active_mode": "schedule",
|
||||||
|
"icon_hash": "",
|
||||||
|
"dev_name": "Smart Wi-Fi Plug",
|
||||||
|
"next_action": {
|
||||||
|
"type": -1
|
||||||
|
},
|
||||||
|
"ntc_state": 0,
|
||||||
|
"err_code": 0
|
||||||
|
*/
|
||||||
|
|
||||||
|
type response struct {
|
||||||
|
System *systemMsg `json:"system,omitempty"`
|
||||||
|
EMeter *eMeterMsg `json:"emeter,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type eMeterMsg struct {
|
||||||
|
GetRealtime *getRealtimeMsg `json:"get_realtime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type getRealtimeMsg struct{}
|
||||||
|
|
||||||
|
type systemMsg struct {
|
||||||
|
GetSysInfo *getSysInfoMsg `json:"get_sysinfo"`
|
||||||
|
SetRelayState *setRelayStateMsg `json:"set_relay_state,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type getSysInfoMsg struct {
|
||||||
|
SWVer string `json:"sw_ver,omitempty"` // e.g. "1.0.3 Build 210506 Rel.105435",
|
||||||
|
HWVer string `json:"hw_ver,omitempty"` // e.g. "1.0",
|
||||||
|
Model string `json:"model,omitempty"` // e.g. "KP105(AU)",
|
||||||
|
DeviceID string `json:"deviceId,omitempty"` // e.g. "8006CF0E89296D2A5BFBDE11C844BB2B1E2D2B7C",
|
||||||
|
OEMID string `json:"oemId,omitempty"` // e.g. "EAB15315847573797920EFE7E9000562",
|
||||||
|
HWID string `json:"hwId,omitempty"` // e.g. "120EF751A1A4F170831E6C1F57D97B38",
|
||||||
|
RSSI int `json:"rssi,omitempty"` // e.g. -58,
|
||||||
|
LongitudeI int `json:"longitude_i,omitempty"` // e.g. 1473005,
|
||||||
|
LatitudeI int `json:"latitude_i,omitempty"` // e.g. -428429,
|
||||||
|
Alias string `json:"alias,omitempty"` // e.g. "Car charger",
|
||||||
|
Status string `json:"status,omitempty"` // e.g. "new",
|
||||||
|
MICType string `json:"mic_type,omitempty"` // e.g. "IOT.SMARTPLUGSWITCH",
|
||||||
|
Feature string `json:"feature,omitempty"` // e.g. "TIM",
|
||||||
|
MAC string `json:"mac,omitempty"` // e.g. "00:5F:67:01:A5:50",
|
||||||
|
Updating int `json:"updating,omitempty"` // e.g. 0,
|
||||||
|
LEDOff int `json:"led_off,omitempty"` // e.g. 0,
|
||||||
|
OBDSrc string `json:"obd_src,omitempty"` // e.g. "tplink",
|
||||||
|
RelayState int `json:"relay_state,omitempty"` // e.g. 0,
|
||||||
|
OnTime int `json:"on_time,omitempty"` // e.g. 0,
|
||||||
|
ActiveMode string `json:"active_mode,omitempty"` // e.g. "schedule",
|
||||||
|
IconHash string `json:"icon_hash,omitempty"` // e.g. "",
|
||||||
|
DevName string `json:"dev_name,omitempty"` // e.g. "Smart Wi-Fi Plug",
|
||||||
|
NextAction struct {
|
||||||
|
Type int `json:"type,omitempty"`
|
||||||
|
} `json:"next_action,omitempty"` // e.g. {"type": -1},
|
||||||
|
NTCState int `json:"ntc_state,omitempty"` // e.g. 0,
|
||||||
|
ErrCode int `json:"err_code,omitempty"` // e.g. 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type setRelayStateMsg struct {
|
||||||
|
ErrCode int `json:"err_code,omitempty"`
|
||||||
|
State int `json:"state,omitempty"`
|
||||||
|
}
|
||||||
|
|
50
plug_test.go
50
plug_test.go
|
@ -2,6 +2,7 @@ package plugctl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"io"
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -14,15 +15,15 @@ func TestDecrypt(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("base64.DecodeString(%q) error: %v", in64, err)
|
t.Fatalf("base64.DecodeString(%q) error: %v", in64, err)
|
||||||
}
|
}
|
||||||
|
t.Logf("in[:4] = %v", in[:4])
|
||||||
in = in[4:]
|
in = in[4:]
|
||||||
want := []byte(`{"system":{"set_relay_state":{"state":1}}}`)
|
want := []byte(`{"system":{"set_relay_state":{"state":1}}}`)
|
||||||
dec := newDecrypter(bytes.NewReader(in))
|
got, err := io.ReadAll(newDecrypter(bytes.NewReader(in)))
|
||||||
got, err := io.ReadAll(dec)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("io.ReadAll(decryptReader) error: %v", err)
|
t.Fatalf("io.ReadAll(decryptReader) error: %v", err)
|
||||||
}
|
}
|
||||||
if !bytes.Equal(got, want) {
|
if !bytes.Equal(got, want) {
|
||||||
t.Errorf("decrypt(%02x) = %q, want %q", in, got, want)
|
t.Errorf("io.ReadAll(decryptReader) = %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,19 +37,40 @@ func TestEncrypt(t *testing.T) {
|
||||||
want = want[4:]
|
want = want[4:]
|
||||||
in := []byte(`{"system":{"set_relay_state":{"state":1}}}`)
|
in := []byte(`{"system":{"set_relay_state":{"state":1}}}`)
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
newEncrypter(&buf).Write(in)
|
if _, err := newEncrypter(&buf).Write(in); err != nil {
|
||||||
|
t.Fatalf("newEncrypter(buf).Write(in) error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if got := buf.Bytes(); !bytes.Equal(got, want) {
|
if got := buf.Bytes(); !bytes.Equal(got, want) {
|
||||||
t.Errorf("encrypt(%q) = %02x, want %02x", in, got, want)
|
t.Errorf("buf.Bytes() = %02x, want %02x", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// func TestEncryptDecrypt(t *testing.T) {
|
func TestEncryptDecrypt(t *testing.T) {
|
||||||
// want := make([]byte, 64)
|
want := make([]byte, 64)
|
||||||
// if _, err := rand.Read(want); err != nil {
|
if _, err := rand.Read(want); err != nil {
|
||||||
// t.Fatalf("rand.Read(want) error: %v", err)
|
t.Fatalf("rand.Read(want) error: %v", err)
|
||||||
// }
|
}
|
||||||
// if got := decrypt(encrypt(want)); !bytes.Equal(got, want) {
|
|
||||||
// t.Errorf("decrypt(encrypt(%02x)) = %02x, want %02x", want, got, want)
|
var buf bytes.Buffer
|
||||||
// }
|
newEncrypter(&buf).Write(want)
|
||||||
// }
|
got, err := io.ReadAll(newDecrypter(&buf))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("io.ReadAll(newDecrypter(&buf))")
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, want) {
|
||||||
|
t.Errorf("decrypt(encrypt(%02x)) = %02x, want %02x", want, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadStatusFromRealPlug(t *testing.T) {
|
||||||
|
//t.Skip("Hits the real device")
|
||||||
|
|
||||||
|
p := Plug{Addr: "192.168.86.10:9999"}
|
||||||
|
|
||||||
|
resp, err := p.roundTrip(reqGetSysinfo)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Plug.roundTrip(%v) error = %v", reqGetSysinfo, err)
|
||||||
|
}
|
||||||
|
t.Logf("Plug.roundTrip(%v) = %#v", reqGetSysinfo, *resp.System.GetSysInfo)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue