// 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"` }