230 lines
6.4 KiB
Go
230 lines
6.4 KiB
Go
/*
|
|
Copyright 2023 Josh Deprez
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
// Package plugctl provides an API for controlling Kasa smart home switches.
|
|
package plugctl
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
)
|
|
|
|
// const reqGetEMeter = `{"emeter":{"get_realtime":null}}`
|
|
|
|
// Plug provides an API for a single Kasa smart home plug or switch.
|
|
type Plug struct {
|
|
Addr string
|
|
}
|
|
|
|
// SetRelayState turns the plug on or off.
|
|
func (p *Plug) SetRelayState(on bool) error {
|
|
state := 0
|
|
if on {
|
|
state = 1
|
|
}
|
|
req := fmt.Sprintf(`{"system":{"set_relay_state":{"state":%d}}}`, state)
|
|
resp, err := p.roundTrip(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp.System == nil {
|
|
return fmt.Errorf("nil system in response")
|
|
}
|
|
if resp.System.SetRelayState == nil {
|
|
return fmt.Errorf("nil set_relay_state in system")
|
|
}
|
|
if errCode := resp.System.SetRelayState.ErrCode; errCode != 0 {
|
|
return fmt.Errorf("plug errcode %d", errCode)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TurnOn turns the plug on.
|
|
func (p *Plug) TurnOn() error {
|
|
return p.SetRelayState(true)
|
|
}
|
|
|
|
// TurnOff turns the plug off.
|
|
func (p *Plug) TurnOff() error {
|
|
return p.SetRelayState(false)
|
|
}
|
|
|
|
// SysInfo gets the system information from the plug.
|
|
func (p *Plug) SysInfo() (*SysInfo, error) {
|
|
resp, err := p.roundTrip(`{"system":{"get_sysinfo":null}}`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.System == nil {
|
|
return nil, fmt.Errorf("nil system in response")
|
|
}
|
|
if resp.System.GetSysInfo == nil {
|
|
return nil, fmt.Errorf("nil get_sysinfo in system")
|
|
}
|
|
return resp.System.GetSysInfo, nil
|
|
}
|
|
|
|
// RelayState reports whether the plug is currently on.
|
|
func (p *Plug) RelayState() (bool, error) {
|
|
si, err := p.SysInfo()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return si.RelayState == 1, nil
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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 *SysInfo `json:"get_sysinfo"`
|
|
SetRelayState *setRelayStateMsg `json:"set_relay_state,omitempty"`
|
|
}
|
|
|
|
type SysInfo 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"`
|
|
}
|