341 lines
7.8 KiB
Go
341 lines
7.8 KiB
Go
package tplink
|
|
|
|
// Credit to:
|
|
// sausheong - https://github.com/sausheong/hs1xxplug
|
|
// jaedle - https://github.com/jaedle/golang-tplink-hs100/blob/master/internal/connector/connector.go
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"time"
|
|
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
)
|
|
|
|
// This is the key by which all bytes sent/received from tp-link hardware are
|
|
// obfuscated.
|
|
const hashKey byte = 0xAB
|
|
|
|
// SysInfo A type used to return information from tplink devices
|
|
type SysInfo struct {
|
|
System struct {
|
|
GetSysinfo struct {
|
|
SwVer string `json:"sw_ver"`
|
|
HwVer string `json:"hw_ver"`
|
|
Type string `json:"type"`
|
|
Model string `json:"model"`
|
|
Mac string `json:"mac"`
|
|
DevName string `json:"dev_name"`
|
|
Alias string `json:"alias"`
|
|
RelayState int `json:"relay_state"`
|
|
OnTime int `json:"on_time"`
|
|
ActiveMode string `json:"active_mode"`
|
|
Feature string `json:"feature"`
|
|
Updating int `json:"updating"`
|
|
IconHash string `json:"icon_hash"`
|
|
Rssi int `json:"rssi"`
|
|
LedOff int `json:"led_off"`
|
|
LongitudeI int `json:"longitude_i"`
|
|
LatitudeI int `json:"latitude_i"`
|
|
HwID string `json:"hwId"`
|
|
FwID string `json:"fwId"`
|
|
DeviceID string `json:"deviceId"`
|
|
OemID string `json:"oemId"`
|
|
Children []struct {
|
|
ID string `json:"id"`
|
|
State int `json:"state"`
|
|
Alias string `json:"alias"`
|
|
OnTime int `json:"on_time"`
|
|
NextAction struct {
|
|
Type int `json:"type"`
|
|
} `json:"next_action"`
|
|
} `json:"children"`
|
|
ChildNum int `json:"child_num"`
|
|
NtcState int `json:"ntc_state"`
|
|
NextAction struct {
|
|
Type int `json:"type"`
|
|
} `json:"next_action"`
|
|
ErrCode int `json:"err_code"`
|
|
MicType string `json:"mic_type"`
|
|
Latitude float64 `json:"latitude"`
|
|
Longitude float64 `json:"longitude"`
|
|
} `json:"get_sysinfo"`
|
|
} `json:"system"`
|
|
Emeter struct {
|
|
GetRealtime struct {
|
|
CurrentMa int `json:"current_ma"`
|
|
VoltageMv int `json:"voltage_mv"`
|
|
PowerMw int `json:"power_mw"`
|
|
TotalWh int `json:"total_wh"`
|
|
ErrCode int `json:"err_code"`
|
|
} `json:"get_realtime"`
|
|
GetVgainIgain struct {
|
|
Vgain int `json:"vgain"`
|
|
Igain int `json:"igain"`
|
|
ErrCode int `json:"err_code"`
|
|
} `json:"get_vgain_igain"`
|
|
GetDaystat struct {
|
|
DayList []struct {
|
|
Year int `json:"year"`
|
|
Month int `json:"month"`
|
|
Day int `json:"day"`
|
|
EnergyWh int `json:"energy_wh"`
|
|
} `json:"day_list"`
|
|
ErrCode int `json:"err_code"`
|
|
} `json:"get_daystat"`
|
|
} `json:"emeter"`
|
|
}
|
|
|
|
// Tplink Device host identification
|
|
type Tplink struct {
|
|
Host string
|
|
SwitchID int
|
|
}
|
|
|
|
type getSysInfo struct {
|
|
System struct {
|
|
SysInfo struct {
|
|
} `json:"get_sysinfo"`
|
|
} `json:"system"`
|
|
}
|
|
|
|
type changeState struct {
|
|
System struct {
|
|
SetRelayState struct {
|
|
State int `json:"state"`
|
|
} `json:"set_relay_state"`
|
|
} `json:"system"`
|
|
}
|
|
|
|
type changeStateMultiSwitch struct {
|
|
Context struct {
|
|
ChildIds []string `json:"child_ids"`
|
|
} `json:"context"`
|
|
System struct {
|
|
SetRelayState struct {
|
|
State int `json:"state"`
|
|
} `json:"set_relay_state"`
|
|
} `json:"system"`
|
|
}
|
|
|
|
type meterInfo struct {
|
|
System struct {
|
|
GetSysinfo struct{} `json:"get_sysinfo"`
|
|
} `json:"system"`
|
|
Emeter struct {
|
|
GetRealtime struct{} `json:"get_realtime"`
|
|
GetVgainIgain struct{} `json:"get_vgain_igain"`
|
|
} `json:"emeter"`
|
|
}
|
|
|
|
type dailyStats struct {
|
|
Emeter struct {
|
|
GetDaystat struct {
|
|
Month int `json:"month"`
|
|
Year int `json:"year"`
|
|
} `json:"get_daystat"`
|
|
} `json:"emeter"`
|
|
}
|
|
|
|
func encrypt(plaintext string) []byte {
|
|
n := len(plaintext)
|
|
buf := new(bytes.Buffer)
|
|
if err := binary.Write(buf, binary.BigEndian, uint32(n)); err != nil {
|
|
log.Printf("[WARNING] tplink.go: Unable to write to buffer %v\n", err)
|
|
}
|
|
ciphertext := buf.Bytes()
|
|
|
|
key := hashKey
|
|
payload := make([]byte, n)
|
|
for i := 0; i < n; i++ {
|
|
payload[i] = plaintext[i] ^ key
|
|
key = payload[i]
|
|
}
|
|
|
|
for i := 0; i < len(payload); i++ {
|
|
ciphertext = append(ciphertext, payload[i])
|
|
}
|
|
|
|
return ciphertext
|
|
}
|
|
|
|
func decrypt(ciphertext []byte) string {
|
|
n := len(ciphertext)
|
|
key := hashKey
|
|
var nextKey byte
|
|
for i := 0; i < n; i++ {
|
|
nextKey = ciphertext[i]
|
|
ciphertext[i] ^= key
|
|
key = nextKey
|
|
}
|
|
return string(ciphertext)
|
|
}
|
|
|
|
func send(host string, dataSend []byte) ([]byte, error) {
|
|
var header = make([]byte, 4)
|
|
|
|
// establish connection (two second timeout)
|
|
conn, err := net.DialTimeout("tcp", host+":9999", time.Second*2) //nolint:gomnd
|
|
if err != nil {
|
|
return []byte{}, err
|
|
}
|
|
defer func() {
|
|
if err := conn.Close(); err != nil { //nolint:govet
|
|
log.Fatalf("[ERROR] tplink.go: Unable to close connection: %v\n", err)
|
|
}
|
|
}()
|
|
|
|
// submit data to device
|
|
writer := bufio.NewWriter(conn)
|
|
_, err = writer.Write(dataSend)
|
|
if err != nil {
|
|
return []byte{}, err
|
|
}
|
|
if err := writer.Flush(); err != nil { //nolint:govet
|
|
return []byte{}, err
|
|
}
|
|
|
|
// read response header to determine response size
|
|
headerReader := io.LimitReader(conn, int64(4)) //nolint:gomnd
|
|
_, err = headerReader.Read(header)
|
|
if err != nil {
|
|
return []byte{}, err
|
|
}
|
|
|
|
// read response
|
|
respSize := int64(binary.BigEndian.Uint32(header))
|
|
respReader := io.LimitReader(conn, respSize)
|
|
var response = make([]byte, respSize)
|
|
_, err = respReader.Read(response)
|
|
if err != nil {
|
|
return []byte{}, err
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
func getDevID(s *Tplink) (string, error) {
|
|
info, err := s.SystemInfo()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return info.System.GetSysinfo.DeviceID, nil
|
|
}
|
|
|
|
// SystemInfo Returns information from targeted device
|
|
func (s *Tplink) SystemInfo() (SysInfo, error) {
|
|
var (
|
|
payload getSysInfo
|
|
jsonResp SysInfo
|
|
)
|
|
|
|
j, _ := json.Marshal(payload)
|
|
|
|
data := encrypt(string(j))
|
|
resp, err := send(s.Host, data)
|
|
if err != nil {
|
|
return jsonResp, err
|
|
}
|
|
|
|
if err := json.Unmarshal([]byte(decrypt(resp)), &jsonResp); err != nil {
|
|
return jsonResp, err
|
|
}
|
|
return jsonResp, nil
|
|
}
|
|
|
|
// ChangeState changes the power state of a single port device
|
|
// True = on
|
|
// False = off
|
|
func (s *Tplink) ChangeState(state bool) error {
|
|
var payload changeState
|
|
|
|
if state {
|
|
payload.System.SetRelayState.State = 1
|
|
} else {
|
|
payload.System.SetRelayState.State = 0
|
|
}
|
|
|
|
j, _ := json.Marshal(payload)
|
|
data := encrypt(string(j))
|
|
if _, err := send(s.Host, data); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ChangeStateMultiSwitch changes the power state of a device on with multiple outlets/switches
|
|
// True = on
|
|
// False = off
|
|
func (s *Tplink) ChangeStateMultiSwitch(state bool) error {
|
|
var payload changeStateMultiSwitch
|
|
|
|
devID, err := getDevID(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
payload.Context.ChildIds = append(payload.Context.ChildIds, devID+fmt.Sprintf("%02d", s.SwitchID))
|
|
if state {
|
|
payload.System.SetRelayState.State = 1
|
|
} else {
|
|
payload.System.SetRelayState.State = 0
|
|
}
|
|
|
|
j, _ := json.Marshal(payload)
|
|
data := encrypt(string(j))
|
|
if _, err := send(s.Host, data); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetMeterInfo gets the power stats from a device
|
|
func (s *Tplink) GetMeterInto() (SysInfo, error) {
|
|
var (
|
|
payload meterInfo
|
|
jsonResp SysInfo
|
|
)
|
|
|
|
j, _ := json.Marshal(payload)
|
|
|
|
data := encrypt(string(j))
|
|
resp, err := send(s.Host, data)
|
|
if err != nil {
|
|
return jsonResp, err
|
|
}
|
|
|
|
if err := json.Unmarshal([]byte(decrypt(resp)), &jsonResp); err != nil {
|
|
return jsonResp, err
|
|
}
|
|
return jsonResp, nil
|
|
}
|
|
|
|
// GetMeterInfo gets the power stats from a device
|
|
func (s *Tplink) GetDailyStats(month, year int) (SysInfo, error) {
|
|
var (
|
|
payload dailyStats
|
|
jsonResp SysInfo
|
|
)
|
|
|
|
payload.Emeter.GetDaystat.Month = month
|
|
payload.Emeter.GetDaystat.Year = year
|
|
|
|
j, _ := json.Marshal(payload)
|
|
|
|
data := encrypt(string(j))
|
|
resp, err := send(s.Host, data)
|
|
if err != nil {
|
|
return jsonResp, err
|
|
}
|
|
|
|
if err := json.Unmarshal([]byte(decrypt(resp)), &jsonResp); err != nil {
|
|
return jsonResp, err
|
|
}
|
|
return jsonResp, nil
|
|
}
|