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 (
|
require (
|
||||||
github.com/eclipse/paho.mqtt.golang v1.3.5
|
github.com/eclipse/paho.mqtt.golang v1.3.5
|
||||||
github.com/joho/godotenv v1.4.0
|
github.com/joho/godotenv v1.4.0
|
||||||
|
github.com/r3labs/sse/v2 v2.8.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gorilla/websocket v1.4.2 // indirect
|
github.com/gorilla/websocket v1.4.2 // indirect
|
||||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 // 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 h1:sWtmgNxYM9P2sP+xEItMozsR3w0cqZFlqnNN1bdl41Y=
|
||||||
github.com/eclipse/paho.mqtt.golang v1.3.5/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc=
|
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 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
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 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
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/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-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 h1:4qWs8cYYH6PoEFy4dfhDFgoMGkwAcETd+MmPdCPMzUc=
|
||||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
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=
|
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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"automation/hue"
|
||||||
"encoding/json"
|
"automation/mqtt"
|
||||||
|
"automation/ntfy"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
MQTT "github.com/eclipse/paho.mqtt.golang"
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This is the default message handler, it just prints out the topic and message
|
func SendCmd(cmd []byte) {
|
||||||
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 main() {
|
func main() {
|
||||||
|
@ -209,38 +23,33 @@ func main() {
|
||||||
signal.Notify(halt, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(halt, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
// MQTT
|
// MQTT
|
||||||
client := connectMQTT()
|
m := mqtt.Connect()
|
||||||
presence := make(chan bool, 1)
|
defer m.Disconnect()
|
||||||
if token := client.Subscribe("automation/presence/+", 0, presenceHandler(presence)); token.Wait() && token.Error() != nil {
|
|
||||||
fmt.Println(token.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
// Hue
|
// Hue
|
||||||
hue := connectToHue()
|
h := hue.Connect()
|
||||||
|
|
||||||
|
// Kasa
|
||||||
|
// k := kasa.New("10.0.0.32")
|
||||||
|
|
||||||
// ntfy.sh
|
// ntfy.sh
|
||||||
ntfy := connectNtfy()
|
n := ntfy.Connect()
|
||||||
|
|
||||||
// Event loop
|
// Event loop
|
||||||
fmt.Println("Starting event loop")
|
fmt.Println("Starting event loop")
|
||||||
events:
|
events:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case present := <-presence:
|
case present := <-m.Presence:
|
||||||
fmt.Printf("Present: %t\n", present)
|
fmt.Printf("Presence: %t\n", present)
|
||||||
hue.updateFlag(41, present)
|
h.SetFlag(41, present)
|
||||||
ntfy.notifyPresence(present)
|
n.Presence(present)
|
||||||
|
|
||||||
|
case <-h.Events:
|
||||||
|
break
|
||||||
|
|
||||||
case <-halt:
|
case <-halt:
|
||||||
break events
|
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