Refactored code and added support for kasa smart plugs
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
76a8c5e620
commit
dace0eba29
2
go.mod
2
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
|
||||
)
|
||||
|
|
15
go.sum
15
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=
|
||||
|
|
60
hue/events.go
Normal file
60
hue/events.go
Normal file
|
@ -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"`
|
||||
}
|
59
hue/hue.go
Normal file
59
hue/hue.go
Normal file
|
@ -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
|
||||
}
|
88
kasa/kasa.go
Normal file
88
kasa/kasa.go
Normal file
|
@ -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)
|
||||
}
|
19
kasa/reply.go
Normal file
19
kasa/reply.go
Normal file
|
@ -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"`
|
||||
}
|
229
main.go
229
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)
|
||||
}
|
||||
|
|
109
mqtt/mqtt.go
Normal file
109
mqtt/mqtt.go
Normal file
|
@ -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)
|
||||
}
|
46
ntfy/ntfy.go
Normal file
46
ntfy/ntfy.go
Normal file
|
@ -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
|
||||
}
|
136
tplink_smartplug.py
Executable file
136
tplink_smartplug.py
Executable file
|
@ -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="<hostname>", required=True,
|
||||
help="Target hostname or IP address", type=validHostname)
|
||||
parser.add_argument("-p", "--port", metavar="<port>", 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="<command>",
|
||||
help="Preset command to send. Choices are: "+", ".join(commands), choices=commands)
|
||||
group.add_argument("-j", "--json", metavar="<JSON string>",
|
||||
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}")
|
||||
|
Reference in New Issue
Block a user