Tapo: Enable KLAP protocol + P100 devices (#10606)
This commit is contained in:
parent
eb54d70e2d
commit
c8c6f7d95b
5 changed files with 64 additions and 421 deletions
|
|
@ -55,17 +55,12 @@ func NewTapo(embed embed, uri, user, password string, standbypower float64) (*Ta
|
|||
|
||||
// Enabled implements the api.Charger interface
|
||||
func (c *Tapo) Enabled() (bool, error) {
|
||||
resp, err := c.conn.ExecCmd("get_device_info", false)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return resp.Result.DeviceON, nil
|
||||
return c.conn.Enabled()
|
||||
}
|
||||
|
||||
// Enable implements the api.Charger interface
|
||||
func (c *Tapo) Enable(enable bool) error {
|
||||
_, err := c.conn.ExecCmd("set_device_info", enable)
|
||||
return err
|
||||
return c.conn.Enable(enable)
|
||||
}
|
||||
|
||||
var _ api.ChargeRater = (*Tapo)(nil)
|
||||
|
|
|
|||
8
go.mod
8
go.mod
|
|
@ -1,6 +1,8 @@
|
|||
module github.com/evcc-io/evcc
|
||||
|
||||
go 1.21
|
||||
go 1.21.1
|
||||
|
||||
toolchain go1.21.3
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.0
|
||||
|
|
@ -47,6 +49,7 @@ require (
|
|||
github.com/hashicorp/go-version v1.6.0
|
||||
github.com/hasura/go-graphql-client v0.10.0
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.12.5-0.20231103130105-12eadbdbefaf
|
||||
github.com/insomniacslk/tapo v0.0.0-20231102105048-52dbe7a83ca7
|
||||
github.com/itchyny/gojq v0.12.13
|
||||
github.com/jeremywohl/flatten v1.0.1
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
|
|
@ -63,7 +66,6 @@ require (
|
|||
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40
|
||||
github.com/mabunixda/wattpilot v1.6.2
|
||||
github.com/manifoldco/promptui v0.9.0
|
||||
github.com/mergermarket/go-pkcs7 v0.0.0-20170926155232-153b18ea13c9
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/mlnoga/rct v0.1.2-0.20230731074838-03eacb926f99
|
||||
|
|
@ -140,6 +142,7 @@ require (
|
|||
github.com/imdario/mergo v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect
|
||||
github.com/insomniacslk/xjson v0.0.0-20231023101448-2249e546a131 // indirect
|
||||
github.com/itchyny/timefmt-go v0.1.5 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
|
|
@ -151,6 +154,7 @@ require (
|
|||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||
github.com/mergermarket/go-pkcs7 v0.0.0-20170926155232-153b18ea13c9 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/miekg/dns v1.1.56 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -424,6 +424,10 @@ github.com/influxdata/influxdb-client-go/v2 v2.12.5-0.20231103130105-12eadbdbefa
|
|||
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf h1:7JTmneyiNEwVBOHSjoMxiWAqB992atOeepeFYegn5RU=
|
||||
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
|
||||
github.com/insomniacslk/tapo v0.0.0-20231102105048-52dbe7a83ca7 h1:HqhTgNBPK6hxSV2Ib+7bZUWNdsIlydgc3Q0T06pEbJc=
|
||||
github.com/insomniacslk/tapo v0.0.0-20231102105048-52dbe7a83ca7/go.mod h1:Ac/4L4hAA14H3yYtFk/ZfgpYZ7knJhNuNKmGl/+YDik=
|
||||
github.com/insomniacslk/xjson v0.0.0-20231023101448-2249e546a131 h1:bVGPuMhjgFtxVdQGfYnFq+EnCqArOAjLNciow/nArwE=
|
||||
github.com/insomniacslk/xjson v0.0.0-20231023101448-2249e546a131/go.mod h1:Z4EVr4bVv9LZbbje9xyZEyOLpdCOmCvr5S9BJtrdTfw=
|
||||
github.com/itchyny/gojq v0.12.13 h1:IxyYlHYIlspQHHTE0f3cJF0NKDMfajxViuhBLnHd/QU=
|
||||
github.com/itchyny/gojq v0.12.13/go.mod h1:JzwzAqenfhrPUuwbmEz3nu3JQmFLlQTQMUcOdnu/Sf4=
|
||||
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
|
||||
|
|
|
|||
|
|
@ -1,49 +1,19 @@
|
|||
package tapo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/mergermarket/go-pkcs7"
|
||||
"github.com/insomniacslk/tapo"
|
||||
)
|
||||
|
||||
// Tapo homepage + api reverse engineering results
|
||||
// https://www.tapo.com/de/
|
||||
// Credits to & inspired by:
|
||||
// https://k4czp3r.xyz/reverse-engineering/tp-link/tapo/2020/10/15/reverse-engineering-tp-link-tapo.html
|
||||
// https://github.com/fishbigger/TapoP100
|
||||
// https://github.com/artemvang/p100-go
|
||||
|
||||
const Timeout = time.Second * 15
|
||||
|
||||
// Connection is the Tapo connection
|
||||
type Connection struct {
|
||||
*request.Helper
|
||||
log *util.Logger
|
||||
URI string
|
||||
EncodedUser string
|
||||
EncodedPassword string
|
||||
Cipher *ConnectionCipher
|
||||
SessionID string
|
||||
Token string
|
||||
TerminalUUID string
|
||||
updated time.Time
|
||||
plug tapo.Plug
|
||||
lasttodayenergy int64
|
||||
energy int64
|
||||
}
|
||||
|
|
@ -52,365 +22,87 @@ type Connection struct {
|
|||
// User is encoded by using MessageDigest of SHA1 which is afterwards B64 encoded.
|
||||
// Password is directly B64 encoded.
|
||||
func NewConnection(uri, user, password string) (*Connection, error) {
|
||||
if uri == "" {
|
||||
return nil, errors.New("missing uri")
|
||||
url, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid url: %s", uri)
|
||||
}
|
||||
|
||||
addr, err := netip.ParseAddr(url.Hostname())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid ip address: %s", uri)
|
||||
}
|
||||
|
||||
if user == "" || password == "" {
|
||||
return nil, fmt.Errorf("missing user or password")
|
||||
}
|
||||
|
||||
for _, suffix := range []string{"/", "/app"} {
|
||||
uri = strings.TrimSuffix(uri, suffix)
|
||||
log := util.NewLogger("tapo").Redact(user, password)
|
||||
|
||||
plug := tapo.NewPlug(addr, nil)
|
||||
if err := plug.Handshake(user, password); err != nil {
|
||||
return nil, fmt.Errorf("login failed: %w", err)
|
||||
}
|
||||
|
||||
log := util.NewLogger("tapo")
|
||||
|
||||
// nosemgrep:go.lang.security.audit.crypto.use_of_weak_crypto.use-of-sha1
|
||||
h := sha1.New()
|
||||
_, err := h.Write([]byte(user))
|
||||
userhash := hex.EncodeToString(h.Sum(nil))
|
||||
|
||||
conn := &Connection{
|
||||
log: log,
|
||||
Helper: request.NewHelper(log),
|
||||
URI: fmt.Sprintf("%s/app", util.DefaultScheme(uri, "http")),
|
||||
EncodedUser: base64.StdEncoding.EncodeToString([]byte(userhash)),
|
||||
EncodedPassword: base64.StdEncoding.EncodeToString([]byte(password)),
|
||||
log: log,
|
||||
plug: *plug,
|
||||
}
|
||||
|
||||
conn.Client.Timeout = Timeout
|
||||
res, err := conn.plug.GetDeviceInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn.log.DEBUG.Printf("%s %s connected (fw:%s,hw:%s,mac:%s)", res.Type, res.Model, res.FWVersion, res.HWVersion, res.MAC)
|
||||
|
||||
return conn, err
|
||||
}
|
||||
|
||||
// Login provides the Tapo device session token and MAC address (TerminalUUID).
|
||||
func (d *Connection) Login() error {
|
||||
err := d.Handshake()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := map[string]interface{}{
|
||||
"method": "login_device",
|
||||
"params": map[string]interface{}{
|
||||
"username": d.EncodedUser,
|
||||
"password": d.EncodedPassword,
|
||||
},
|
||||
}
|
||||
|
||||
res, err := d.DoSecureRequest(d.URI, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := d.CheckErrorCode(res.ErrorCode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.Token = res.Result.Token
|
||||
|
||||
deviceResponse, err := d.ExecMethod("get_device_info", false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.TerminalUUID = deviceResponse.Result.MAC
|
||||
|
||||
return nil
|
||||
// Enable implements the api.Charger interface
|
||||
func (c *Connection) Enable(enable bool) error {
|
||||
return c.plug.SetDeviceInfo(enable)
|
||||
}
|
||||
|
||||
// Handshake provides the Tapo device session cookie and encryption cipher.
|
||||
func (d *Connection) Handshake() error {
|
||||
privKey, pubKey, err := GenerateRSAKeys()
|
||||
// Enabled implements the api.Charger interface
|
||||
func (c *Connection) Enabled() (bool, error) {
|
||||
resp, err := c.plug.GetDeviceInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
pubPEM, err := DumpRSAPEM(pubKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := json.Marshal(map[string]interface{}{
|
||||
"method": "handshake",
|
||||
"params": map[string]interface{}{
|
||||
"key": string(pubPEM),
|
||||
"requestTimeMils": 0,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := http.Post(d.URI, "application/json", bytes.NewBuffer(req))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var res DeviceResponse
|
||||
if err = json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = d.CheckErrorCode(res.ErrorCode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encryptedEncryptionKey, err := base64.StdEncoding.DecodeString(res.Result.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encryptionKey, err := rsa.DecryptPKCS1v15(rand.Reader, privKey, encryptedEncryptionKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.Cipher = &ConnectionCipher{
|
||||
Key: encryptionKey[:16],
|
||||
Iv: encryptionKey[16:],
|
||||
}
|
||||
|
||||
cookie := strings.Split(resp.Header.Get("Set-Cookie"), ";")
|
||||
if len(cookie) == 0 {
|
||||
return errors.New("missing session cookie")
|
||||
}
|
||||
|
||||
d.SessionID = cookie[0]
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExecMethod executes a Tapo device command method and provides the corresponding response.
|
||||
func (d *Connection) ExecMethod(method string, deviceOn bool) (*DeviceResponse, error) {
|
||||
var req map[string]interface{}
|
||||
|
||||
switch method {
|
||||
case "set_device_info":
|
||||
req = map[string]interface{}{
|
||||
"method": method,
|
||||
"params": map[string]interface{}{
|
||||
"device_on": deviceOn,
|
||||
},
|
||||
"requestTimeMils": int(time.Now().Unix() * 1000),
|
||||
"terminalUUID": d.TerminalUUID,
|
||||
}
|
||||
default:
|
||||
req = map[string]interface{}{
|
||||
"method": method,
|
||||
"requestTimeMils": int(time.Now().Unix() * 1000),
|
||||
}
|
||||
}
|
||||
|
||||
res, err := d.DoSecureRequest(fmt.Sprintf("%s?token=%s", d.URI, d.Token), req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if method == "get_device_info" {
|
||||
res.Result.Nickname, err = base64Decode(res.Result.Nickname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.Result.SSID, err = base64Decode(res.Result.SSID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ExecCmd executes a Tapo api command and provides the response
|
||||
func (d *Connection) ExecCmd(method string, enable bool) (*DeviceResponse, error) {
|
||||
// refresh session id
|
||||
if time.Since(d.updated) >= 600*time.Minute {
|
||||
if err := d.Login(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.updated = time.Now()
|
||||
}
|
||||
|
||||
return d.ExecMethod(method, enable)
|
||||
return resp.DeviceON, nil
|
||||
}
|
||||
|
||||
// CurrentPower provides current power consuption
|
||||
func (d *Connection) CurrentPower() (float64, error) {
|
||||
resp, err := d.ExecCmd("get_energy_usage", false)
|
||||
func (c *Connection) CurrentPower() (float64, error) {
|
||||
resp, err := c.plug.GetEnergyUsage()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
if strings.Contains(err.Error(), "-1001") {
|
||||
c.log.DEBUG.Printf("meter not available")
|
||||
return 0, nil
|
||||
} else {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
return float64(resp.Result.Current_Power) / 1e3, nil
|
||||
return float64(resp.CurrentPower) / 1e3, nil
|
||||
}
|
||||
|
||||
// ChargedEnergy collects the daily charged energy
|
||||
func (d *Connection) ChargedEnergy() (float64, error) {
|
||||
resp, err := d.ExecCmd("get_energy_usage", false)
|
||||
func (c *Connection) ChargedEnergy() (float64, error) {
|
||||
resp, err := c.plug.GetEnergyUsage()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if resp.Result.Today_Energy > d.lasttodayenergy {
|
||||
d.energy = d.energy + (resp.Result.Today_Energy - d.lasttodayenergy)
|
||||
}
|
||||
d.lasttodayenergy = resp.Result.Today_Energy
|
||||
|
||||
return float64(d.energy) / 1000, nil
|
||||
}
|
||||
|
||||
// DoSecureRequest executes a Tapo device request by encding the request and decoding its response.
|
||||
func (d *Connection) DoSecureRequest(uri string, taporequest map[string]interface{}) (*DeviceResponse, error) {
|
||||
payload, err := json.Marshal(taporequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.log.TRACE.Printf("request: %s", string(payload))
|
||||
|
||||
encryptedRequest, err := d.Cipher.Encrypt(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"method": "securePassthrough",
|
||||
"params": map[string]interface{}{
|
||||
"request": base64.StdEncoding.EncodeToString(encryptedRequest),
|
||||
},
|
||||
}
|
||||
|
||||
req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), map[string]string{
|
||||
"Cookie": d.SessionID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res *DeviceResponse
|
||||
if err := d.DoJSON(req, &res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Login atempt in case of tapo switch connection hicups
|
||||
if res.ErrorCode == 9999 {
|
||||
if err := d.Login(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := d.DoJSON(req, &res); err != nil {
|
||||
return nil, err
|
||||
if strings.Contains(err.Error(), "-1001") {
|
||||
c.log.DEBUG.Printf("meter not available")
|
||||
return 0, nil
|
||||
} else {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := d.CheckErrorCode(res.ErrorCode); err != nil {
|
||||
return nil, err
|
||||
if int64(resp.TodayEnergy) > c.lasttodayenergy {
|
||||
c.energy = c.energy + (int64(resp.TodayEnergy) - c.lasttodayenergy)
|
||||
}
|
||||
c.lasttodayenergy = int64(resp.TodayEnergy)
|
||||
|
||||
decodedResponse, err := base64.StdEncoding.DecodeString(res.Result.Response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decryptedResponse, err := d.Cipher.Decrypt(decodedResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.log.TRACE.Printf("decrypted result: %v", string(decryptedResponse))
|
||||
|
||||
var deviceResp *DeviceResponse
|
||||
err = json.Unmarshal(decryptedResponse, &deviceResp)
|
||||
|
||||
return deviceResp, err
|
||||
}
|
||||
|
||||
// Tapo helper functions
|
||||
|
||||
func (d *Connection) CheckErrorCode(errorCode int) error {
|
||||
errorDesc := map[int]string{
|
||||
0: "Success",
|
||||
9999: "Login failed, invalid user or password",
|
||||
-1002: "Incorrect Request/Method",
|
||||
-1003: "JSON formatting error ",
|
||||
-1010: "Invalid Public Key Length",
|
||||
-1012: "Invalid terminalUUID",
|
||||
-1501: "Invalid Request or Credentials",
|
||||
}
|
||||
|
||||
if errorCode != 0 {
|
||||
return fmt.Errorf("tapo error %d: %s", errorCode, errorDesc[errorCode])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ConnectionCipher) Encrypt(payload []byte) ([]byte, error) {
|
||||
paddedPayload, err := pkcs7.Pad(payload, aes.BlockSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(c.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encrypter := cipher.NewCBCEncrypter(block, c.Iv)
|
||||
encryptedPayload := make([]byte, len(paddedPayload))
|
||||
encrypter.CryptBlocks(encryptedPayload, paddedPayload)
|
||||
|
||||
return encryptedPayload, nil
|
||||
}
|
||||
|
||||
func (c *ConnectionCipher) Decrypt(payload []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(c.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encrypter := cipher.NewCBCDecrypter(block, c.Iv)
|
||||
decryptedPayload := make([]byte, len(payload))
|
||||
|
||||
encrypter.CryptBlocks(decryptedPayload, payload)
|
||||
|
||||
return pkcs7.Unpad(decryptedPayload, aes.BlockSize)
|
||||
}
|
||||
|
||||
func DumpRSAPEM(pubKey *rsa.PublicKey) ([]byte, error) {
|
||||
pubKeyPKIX, err := x509.MarshalPKIXPublicKey(pubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pubPEM := pem.EncodeToMemory(
|
||||
&pem.Block{
|
||||
Type: "PUBLIC KEY",
|
||||
Bytes: pubKeyPKIX,
|
||||
},
|
||||
)
|
||||
|
||||
return pubPEM, nil
|
||||
}
|
||||
|
||||
func GenerateRSAKeys() (*rsa.PrivateKey, *rsa.PublicKey, error) {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return key, key.Public().(*rsa.PublicKey), nil
|
||||
}
|
||||
|
||||
func base64Decode(base64String string) (string, error) {
|
||||
decodedString, err := base64.StdEncoding.DecodeString(base64String)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(decodedString), nil
|
||||
return float64(c.energy) / 1000, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
package tapo
|
||||
|
||||
// Tapo homepage + api reverse engineering results
|
||||
// https://www.tapo.com/de/
|
||||
// Credits to & inspired by:
|
||||
// https://k4czp3r.xyz/reverse-engineering/tp-link/tapo/2020/10/15/reverse-engineering-tp-link-tapo.html
|
||||
// https://github.com/fishbigger/TapoP100
|
||||
// https://github.com/artemvang/p100-go
|
||||
|
||||
// Tapo connection cipher
|
||||
type ConnectionCipher struct {
|
||||
Key []byte
|
||||
Iv []byte
|
||||
}
|
||||
|
||||
// Tapo device response
|
||||
type DeviceResponse struct {
|
||||
Result struct {
|
||||
DeviceID string `json:"device_id"`
|
||||
FWVersion string `json:"fw_ver"`
|
||||
HWVersion string `json:"hw_ver"`
|
||||
Type string `json:"type"`
|
||||
Model string `json:"model"`
|
||||
MAC string `json:"mac"`
|
||||
HWID string `json:"hw_id"`
|
||||
FWID string `json:"fw_id"`
|
||||
OEMID string `json:"oem_id"`
|
||||
Specs string `json:"specs"`
|
||||
DeviceON bool `json:"device_on"`
|
||||
OnTime int64 `json:"on_time"`
|
||||
OverHeated bool `json:"overheated"`
|
||||
Nickname string `json:"nickname"`
|
||||
Location string `json:"location"`
|
||||
Avatar string `json:"avatar"`
|
||||
Longitude int64 `json:"longitude"`
|
||||
Latitude int64 `json:"latitude"`
|
||||
HasSetLocationInfo bool `json:"has_set_location_info"`
|
||||
IP string `json:"ip"`
|
||||
SSID string `json:"ssid"`
|
||||
SignalLevel int64 `json:"signal_level"`
|
||||
RSSI int64 `json:"rssi"`
|
||||
Region string `json:"Europe/Kiev"`
|
||||
TimeDiff int64 `json:"time_diff"`
|
||||
Lang string `json:"lang"`
|
||||
Key string `json:"key"`
|
||||
Response string `json:"response"`
|
||||
Token string `json:"token"`
|
||||
Current_Power int64 `json:"current_power"`
|
||||
Today_Energy int64 `json:"today_energy"`
|
||||
} `json:"result"`
|
||||
ErrorCode int `json:"error_code"`
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue