From dace0eba2949ec580928ba6ab7f84dbf6aca0e5d Mon Sep 17 00:00:00 2001 From: Dreaded_X Date: Sat, 24 Sep 2022 00:42:42 +0200 Subject: [PATCH] Refactored code and added support for kasa smart plugs --- go.mod | 2 + go.sum | 15 +++ hue/events.go | 60 ++++++++++++ hue/hue.go | 59 ++++++++++++ kasa/kasa.go | 88 +++++++++++++++++ kasa/reply.go | 19 ++++ main.go | 229 ++++---------------------------------------- mqtt/mqtt.go | 109 +++++++++++++++++++++ ntfy/ntfy.go | 46 +++++++++ tplink_smartplug.py | 136 ++++++++++++++++++++++++++ 10 files changed, 553 insertions(+), 210 deletions(-) create mode 100644 hue/events.go create mode 100644 hue/hue.go create mode 100644 kasa/kasa.go create mode 100644 kasa/reply.go create mode 100644 mqtt/mqtt.go create mode 100644 ntfy/ntfy.go create mode 100755 tplink_smartplug.py diff --git a/go.mod b/go.mod index ac6f18a..db5093d 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,11 @@ go 1.17 require ( github.com/eclipse/paho.mqtt.golang v1.3.5 github.com/joho/godotenv v1.4.0 + github.com/r3labs/sse/v2 v2.8.1 ) require ( github.com/gorilla/websocket v1.4.2 // indirect golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 // indirect + gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect ) diff --git a/go.sum b/go.sum index 3a4292f..7c6c528 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,20 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/eclipse/paho.mqtt.golang v1.3.5 h1:sWtmgNxYM9P2sP+xEItMozsR3w0cqZFlqnNN1bdl41Y= github.com/eclipse/paho.mqtt.golang v1.3.5/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/r3labs/sse/v2 v2.8.1 h1:lZH+W4XOLIq88U5MIHOsLec7+R62uhz3bIi2yn0Sg8o= +github.com/r3labs/sse/v2 v2.8.1/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 h1:4qWs8cYYH6PoEFy4dfhDFgoMGkwAcETd+MmPdCPMzUc= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= @@ -16,3 +26,8 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= +gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hue/events.go b/hue/events.go new file mode 100644 index 0000000..89fb539 --- /dev/null +++ b/hue/events.go @@ -0,0 +1,60 @@ +package hue + +import ( + "time" +) + +type EventType string + +const ( + Update EventType = "update" +) + +type DeviceType string + +const ( + Light DeviceType = "light" + GroupedLight = "grouped_light" + Button = "button" +) + +type LastEvent string + +const ( + InitialPress LastEvent = "initial_press" + ShortPress = "short_press" +) + +type device struct { + ID string `json:"id"` + IDv1 string `json:"id_v1"` + Owner struct { + Rid string `json:"rid"` + Rtype string `json:"rtype"` + } `json:"owner"` + Type DeviceType `json:"type"` + + On *struct { + On bool `json:"on"` + } `json:"on"` + + Dimming *struct { + Brightness float32 `json:"brightness"` + } `json:"dimming"` + + ColorTemperature *struct { + Mirek int `json:"mirek"` + MirekValid bool `json:"mirek_valid"` + } `json:"color_temperature"` + + Button *struct { + LastEvent LastEvent `json:"last_event"` + } +} + +type Event struct { + CreationTime time.Time `json:"creationtime"` + Data []device `json:"data"` + ID string `json:"id"` + Type EventType `json:"type"` +} diff --git a/hue/hue.go b/hue/hue.go new file mode 100644 index 0000000..3923007 --- /dev/null +++ b/hue/hue.go @@ -0,0 +1,59 @@ +package hue + +import ( + "bytes" + "crypto/tls" + "fmt" + "net/http" + "os" + + "github.com/r3labs/sse/v2" +) + +type Hue struct { + ip string + login string + Events chan *sse.Event +} + +func (hue *Hue) SetFlag(id int, value bool) { + url := fmt.Sprintf("http://%s/api/%s/sensors/%d/state", hue.ip, hue.login, id) + + var data []byte + if value { + data = []byte(`{ "flag": true }`) + } else { + data = []byte(`{ "flag": false }`) + } + + client := &http.Client{} + req, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(data)) + if err != nil { + panic(err) + } + + _, err = client.Do(req) + if err != nil { + panic(err) + } +} + +func Connect() Hue { + login, _ := os.LookupEnv("HUE_BRIDGE") + + hue := Hue{ip: "10.0.0.146", login: login, Events: make(chan *sse.Event)} + + // Subscribe to eventstream + client := sse.NewClient(fmt.Sprintf("https://%s/eventstream/clip/v2", hue.ip)) + client.Headers["hue-application-key"] = hue.login + client.Connection.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + err := client.SubscribeChanRaw(hue.Events) + if err != nil { + panic(err) + } + + return hue +} diff --git a/kasa/kasa.go b/kasa/kasa.go new file mode 100644 index 0000000..6b9309c --- /dev/null +++ b/kasa/kasa.go @@ -0,0 +1,88 @@ +package kasa + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "fmt" + "net" +) + +func encrypt(data []byte) []byte { + var key byte = 171 + buf := new(bytes.Buffer) + binary.Write(buf, binary.BigEndian, uint32(len(data))) + + for _, c := range []byte(data) { + a := key ^ c + key = a + buf.WriteByte(a) + } + + return buf.Bytes() +} + +func decrypt(data []byte) string { + var key byte = 171 + buf := new(bytes.Buffer) + binary.Write(buf, binary.BigEndian, uint32(len(data))) + + for _, c := range data { + a := key ^ c + key = c + buf.WriteByte(a) + } + + return string(buf.Bytes()) +} + + +type Kasa struct { + ip string +} + +func New(ip string) Kasa { + return Kasa{ip} +} + +func (kasa *Kasa) sendCmd(cmd cmd) { + con, err := net.Dial("tcp", fmt.Sprintf("%s:9999", kasa.ip)) + if err != nil { + panic(err) + } + + defer con.Close() + + b, err := json.Marshal(cmd) + if err != nil { + panic(err) + } + + _, err = con.Write(encrypt(b)) + if err != nil { + panic(err) + } + + resp := make([]byte, 2048) + _, err = con.Read(resp) + if err != nil { + panic(err) + } + + var reply reply + json.Unmarshal(resp, &reply) + + if reply.System.SetRelayState.ErrCode != 0 { + fmt.Println(reply) + fmt.Println(resp) + } +} + +func (kasa *Kasa) SetState(on bool) { + var cmd cmd + if on { + cmd.System.SetRelayState.State = 1 + } + + kasa.sendCmd(cmd) +} diff --git a/kasa/reply.go b/kasa/reply.go new file mode 100644 index 0000000..fc279ec --- /dev/null +++ b/kasa/reply.go @@ -0,0 +1,19 @@ +package kasa + +type errCode struct { + ErrCode int +} + +type reply struct { + System struct { + SetRelayState errCode `json:"set_relay_state"` + } `json:"system"` +} + +type cmd struct { + System struct { + SetRelayState struct { + State int `json:"state"` + } `json:"set_relay_state"` + } `json:"system"` +} diff --git a/main.go b/main.go index 8358316..250d28c 100644 --- a/main.go +++ b/main.go @@ -1,204 +1,18 @@ package main import ( - "bytes" - "encoding/json" + "automation/hue" + "automation/mqtt" + "automation/ntfy" "fmt" - "io/ioutil" - "net/http" "os" "os/signal" - "strconv" - "strings" "syscall" - MQTT "github.com/eclipse/paho.mqtt.golang" "github.com/joho/godotenv" ) -// This is the default message handler, it just prints out the topic and message -var defaultHandler MQTT.MessageHandler = func(client MQTT.Client, msg MQTT.Message) { - fmt.Printf("TOPIC: %s\n", msg.Topic()) - fmt.Printf("MSG: %s\n", msg.Payload()) -} - -// Handler got automation/presence/+ -func presenceHandler(presence chan bool) func(MQTT.Client, MQTT.Message) { - devices := make(map[string]bool) - var current *bool - - return func(client MQTT.Client, msg MQTT.Message) { - name := strings.Split(msg.Topic(), "/")[2] - if len(msg.Payload()) == 0 { - // @TODO What happens if we delete a device that does not exist - delete(devices, name) - } else { - value, err := strconv.Atoi(string(msg.Payload())) - if err != nil { - panic(err) - } - - devices[name] = value == 1 - } - - present := false - fmt.Println(devices) - for _, value := range devices { - if value { - present = true - break; - } - } - - if current == nil || *current != present { - current = &present - presence <- present - } - - } -} - -type Hue struct { - ip string - login string -} - -func (hue *Hue) updateFlag(id int, value bool) { - url := fmt.Sprintf("http://%s/api/%s/sensors/%d/state", hue.ip, hue.login, id) - - var data []byte - if value { - data = []byte(`{ "flag": true }`) - } else { - data = []byte(`{ "flag": false }`) - } - - client := &http.Client{} - req, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(data)) - if err != nil { - panic(err) - } - - _, err = client.Do(req) - if err != nil { - panic(err) - } -} - -type ntfy struct{ - topic string -} - -func (ntfy *ntfy) notifyPresence(home bool) { - // @TODO Maybe add list the devices that are home currently? - var description string - var actions string - if home { - description = "Home" - actions = "broadcast, Set as away, extras.cmd=presence, extras.state=0, clear=true" - } else { - description = "Away" - actions = "broadcast, Set as home, extras.cmd=presence, extras.state=1, clear=true" - } - - req, err := http.NewRequest("POST", fmt.Sprintf("https://ntfy.sh/%s", ntfy.topic), strings.NewReader(description)) - if err != nil { - panic(err) - } - - req.Header.Set("Title", "Presence") - req.Header.Set("Tags", "house") - req.Header.Set("Actions", actions) - req.Header.Set("Priority", "1") - - http.DefaultClient.Do(req) -} - -func connectToHue() Hue { - login, _ := os.LookupEnv("HUE_BRIDGE") - - resp, err := http.Get("https://discovery.meethue.com/") - if err != nil { - panic(err) - } - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - panic(err) - } - - var bridges []struct { - ID string `json:"id"` - InternalIPAddress string `json:"internalipaddress"` - Port int `json:"port"` - } - err = json.Unmarshal(body, &bridges) - if err != nil { - panic(err) - } - - if len(bridges) != 1 { - fmt.Println(bridges) - panic("Expected one bridge!") - } - - hue := Hue{ip: bridges[0].InternalIPAddress, login: login} - - resp, err = http.Get("https://discovery.meethue.com/") - if err != nil { - panic(err) - } - - if resp.Status != "200 OK" { - panic("Check failed") - } - - return hue -} - -func connectMQTT() MQTT.Client { - host, ok := os.LookupEnv("MQTT_HOST") - if !ok { - host = "localhost" - } - port, ok := os.LookupEnv("MQTT_PORT") - if !ok { - port = "1883" - } - user, ok := os.LookupEnv("MQTT_USER") - if !ok { - user = "test" - } - pass, ok := os.LookupEnv("MQTT_PASS") - if !ok { - pass = "test" - } - clientID, ok := os.LookupEnv("MQTT_CLIENT_ID") - if !ok { - clientID = "automation" - } - - opts := MQTT.NewClientOptions().AddBroker(fmt.Sprintf("%s:%s", host, port)) - opts.SetClientID(clientID) - opts.SetDefaultPublishHandler(defaultHandler) - opts.SetUsername(user) - opts.SetPassword(pass) - - client := MQTT.NewClient(opts) - if token := client.Connect(); token.Wait() && token.Error() != nil { - panic(token.Error()) - } - - return client -} - -func connectNtfy() ntfy { - topic, _ := os.LookupEnv("NTFY_TOPIC") - ntfy := ntfy{topic} - - // @TODO Make sure the topic is valid? - - return ntfy +func SendCmd(cmd []byte) { } func main() { @@ -209,38 +23,33 @@ func main() { signal.Notify(halt, os.Interrupt, syscall.SIGTERM) // MQTT - client := connectMQTT() - presence := make(chan bool, 1) - if token := client.Subscribe("automation/presence/+", 0, presenceHandler(presence)); token.Wait() && token.Error() != nil { - fmt.Println(token.Error()) - os.Exit(1) - } + m := mqtt.Connect() + defer m.Disconnect() + // Hue - hue := connectToHue() + h := hue.Connect() + + // Kasa + // k := kasa.New("10.0.0.32") // ntfy.sh - ntfy := connectNtfy() + n := ntfy.Connect() // Event loop fmt.Println("Starting event loop") events: for { select { - case present := <-presence: - fmt.Printf("Present: %t\n", present) - hue.updateFlag(41, present) - ntfy.notifyPresence(present) + case present := <-m.Presence: + fmt.Printf("Presence: %t\n", present) + h.SetFlag(41, present) + n.Presence(present) + + case <-h.Events: + break case <-halt: break events } } - - // Cleanup - if token := client.Unsubscribe("automation/presence/+"); token.Wait() && token.Error() != nil { - fmt.Println(token.Error()) - os.Exit(1) - } - - client.Disconnect(250) } diff --git a/mqtt/mqtt.go b/mqtt/mqtt.go new file mode 100644 index 0000000..79e71e4 --- /dev/null +++ b/mqtt/mqtt.go @@ -0,0 +1,109 @@ +package mqtt + +import ( + "fmt" + "os" + "strconv" + "strings" + + "github.com/eclipse/paho.mqtt.golang" +) + +type MQTT struct { + Presence chan bool + client mqtt.Client +} + +// This is the default message handler, it just prints out the topic and message +var defaultHandler mqtt.MessageHandler = func(client mqtt.Client, msg mqtt.Message) { + fmt.Printf("TOPIC: %s\n", msg.Topic()) + fmt.Printf("MSG: %s\n", msg.Payload()) +} + +// Handler got automation/presence/+ +func presenceHandler(presence chan bool) func(mqtt.Client, mqtt.Message) { + devices := make(map[string]bool) + var current *bool + + return func(client mqtt.Client, msg mqtt.Message) { + name := strings.Split(msg.Topic(), "/")[2] + if len(msg.Payload()) == 0 { + // @TODO What happens if we delete a device that does not exist + delete(devices, name) + } else { + value, err := strconv.Atoi(string(msg.Payload())) + if err != nil { + panic(err) + } + + devices[name] = value == 1 + } + + present := false + fmt.Println(devices) + for _, value := range devices { + if value { + present = true + break + } + } + + if current == nil || *current != present { + current = &present + presence <- present + } + + } +} + +func Connect() MQTT { + host, ok := os.LookupEnv("MQTT_HOST") + if !ok { + host = "localhost" + } + port, ok := os.LookupEnv("MQTT_PORT") + if !ok { + port = "1883" + } + user, ok := os.LookupEnv("MQTT_USER") + if !ok { + user = "test" + } + pass, ok := os.LookupEnv("MQTT_PASS") + if !ok { + pass = "test" + } + clientID, ok := os.LookupEnv("MQTT_CLIENT_ID") + if !ok { + clientID = "automation" + } + + opts := mqtt.NewClientOptions().AddBroker(fmt.Sprintf("%s:%s", host, port)) + opts.SetClientID(clientID) + opts.SetDefaultPublishHandler(defaultHandler) + opts.SetUsername(user) + opts.SetPassword(pass) + + client := mqtt.NewClient(opts) + if token := client.Connect(); token.Wait() && token.Error() != nil { + panic(token.Error()) + } + + m := MQTT{client: client, Presence: make(chan bool)} + + if token := client.Subscribe("automation/presence/+", 0, presenceHandler(m.Presence)); token.Wait() && token.Error() != nil { + fmt.Println(token.Error()) + os.Exit(1) + } + + return m +} + +func (m *MQTT) Disconnect() { + if token := m.client.Unsubscribe("automation/presence/+"); token.Wait() && token.Error() != nil { + fmt.Println(token.Error()) + os.Exit(1) + } + + m.client.Disconnect(250) +} diff --git a/ntfy/ntfy.go b/ntfy/ntfy.go new file mode 100644 index 0000000..6c72767 --- /dev/null +++ b/ntfy/ntfy.go @@ -0,0 +1,46 @@ +package ntfy + +import ( + "fmt" + "net/http" + "os" + "strings" +) + +type ntfy struct { + topic string +} + +func (ntfy *ntfy) Presence(home bool) { + // @TODO Maybe add list the devices that are home currently? + var description string + var actions string + if home { + description = "Home" + actions = "broadcast, Set as away, extras.cmd=presence, extras.state=0, clear=true" + } else { + description = "Away" + actions = "broadcast, Set as home, extras.cmd=presence, extras.state=1, clear=true" + } + + req, err := http.NewRequest("POST", fmt.Sprintf("https://ntfy.sh/%s", ntfy.topic), strings.NewReader(description)) + if err != nil { + panic(err) + } + + req.Header.Set("Title", "Presence") + req.Header.Set("Tags", "house") + req.Header.Set("Actions", actions) + req.Header.Set("Priority", "1") + + http.DefaultClient.Do(req) +} + +func Connect() ntfy { + topic, _ := os.LookupEnv("NTFY_TOPIC") + ntfy := ntfy{topic} + + // @TODO Make sure the topic is valid? + + return ntfy +} diff --git a/tplink_smartplug.py b/tplink_smartplug.py new file mode 100755 index 0000000..2622007 --- /dev/null +++ b/tplink_smartplug.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +# +# TP-Link Wi-Fi Smart Plug Protocol Client +# For use with TP-Link HS-100 or HS-110 +# +# by Lubomir Stroetmann +# Copyright 2016 softScheck GmbH +# +# 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. +# + +import argparse +import socket +from struct import pack + +version = 0.4 + +# Check if hostname is valid +def validHostname(hostname): + try: + socket.gethostbyname(hostname) + except socket.error: + parser.error("Invalid hostname.") + return hostname + +# Check if port is valid +def validPort(port): + try: + port = int(port) + except ValueError: + parser.error("Invalid port number.") + + if ((port <= 1024) or (port > 65535)): + parser.error("Invalid port number.") + + return port + + +# Predefined Smart Plug Commands +# For a full list of commands, consult tplink_commands.txt +commands = {'info' : '{"system":{"get_sysinfo":{}}}', + 'on' : '{"system":{"set_relay_state":{"state":1}}}', + 'off' : '{"system":{"set_relay_state":{"state":0}}}', + 'ledoff' : '{"system":{"set_led_off":{"off":1}}}', + 'ledon' : '{"system":{"set_led_off":{"off":0}}}', + 'cloudinfo': '{"cnCloud":{"get_info":{}}}', + 'wlanscan' : '{"netif":{"get_scaninfo":{"refresh":0}}}', + 'time' : '{"time":{"get_time":{}}}', + 'schedule' : '{"schedule":{"get_rules":{}}}', + 'countdown': '{"count_down":{"get_rules":{}}}', + 'antitheft': '{"anti_theft":{"get_rules":{}}}', + 'reboot' : '{"system":{"reboot":{"delay":1}}}', + 'reset' : '{"system":{"reset":{"delay":1}}}', + 'energy' : '{"emeter":{"get_realtime":{}}}' +} + +# Encryption and Decryption of TP-Link Smart Home Protocol +# XOR Autokey Cipher with starting key = 171 + +def encrypt(string): + key = 171 + result = pack(">I", len(string)) + for i in string: + a = key ^ ord(i) + key = a + result += bytes([a]) + return result + +def decrypt(string): + key = 171 + result = "" + for i in string: + a = key ^ i + key = i + result += chr(a) + return result + + +# Parse commandline arguments +parser = argparse.ArgumentParser(description=f"TP-Link Wi-Fi Smart Plug Client v{version}") +parser.add_argument("-t", "--target", metavar="", required=True, + help="Target hostname or IP address", type=validHostname) +parser.add_argument("-p", "--port", metavar="", default=9999, + required=False, help="Target port", type=validPort) +parser.add_argument("-q", "--quiet", dest="quiet", action="store_true", + help="Only show result") +parser.add_argument("--timeout", default=10, required=False, + help="Timeout to establish connection") +group = parser.add_mutually_exclusive_group(required=True) +group.add_argument("-c", "--command", metavar="", + help="Preset command to send. Choices are: "+", ".join(commands), choices=commands) +group.add_argument("-j", "--json", metavar="", + help="Full JSON string of command to send") +args = parser.parse_args() + + +# Set target IP, port and command to send +ip = args.target +port = args.port +if args.command is None: + cmd = args.json +else: + cmd = commands[args.command] + + +# Send command and receive reply +try: + sock_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock_tcp.settimeout(int(args.timeout)) + sock_tcp.connect((ip, port)) + sock_tcp.settimeout(None) + sock_tcp.send(encrypt(cmd)) + data = sock_tcp.recv(2048) + sock_tcp.close() + + decrypted = decrypt(data[4:]) + + if args.quiet: + print(decrypted) + else: + print("Sent: ", cmd) + print("Received: ", decrypted) + +except socket.error: + quit(f"Could not connect to host {ip}:{port}") +