plugctl/plug.go
2023-01-06 17:52:03 +11:00

192 lines
5.3 KiB
Go

// Package plugctl provides an API for controlling Kasa smart home switches.
package plugctl
import (
"bytes"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"net"
)
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 {
Addr string
}
func (p *Plug) roundTrip(req string) (*response, error) {
conn, err := net.Dial("tcp", p.Addr)
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)
}
// Read length of response
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)
}
// Decrypt and parse the response
var resp *response
dec := json.NewDecoder(newDecrypter(bytes.NewReader(buf)))
if err := dec.Decode(&resp); err != nil {
return nil, fmt.Errorf("reading message: %w", err)
}
return resp, nil
}
// Implements TP Link's weaksauce encryption.
// Do NOT use for actually important secrets.
type decryptReader struct {
r io.Reader
c byte
}
func newDecrypter(r io.Reader) *decryptReader {
return &decryptReader{
r: r,
c: 0xab,
}
}
func (d *decryptReader) Read(b []byte) (int, error) {
n, err := d.r.Read(b)
for i, x := range b {
b[i] = d.c ^ x
d.c = x
}
return n, err
}
// Implements TP Link's weaksauce encryption.
// Do NOT use for actually important secrets.
type encryptWriter struct {
w io.Writer
c byte
}
func newEncrypter(w io.Writer) *encryptWriter {
return &encryptWriter{
w: w,
c: 0xab,
}
}
func (e *encryptWriter) Write(b []byte) (int, error) {
eb := make([]byte, len(b))
for i, x := range b {
e.c ^= x
eb[i] = e.c
}
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"`
}