Compare commits
1 Commits
master
...
feature/ss
Author | SHA1 | Date | |
---|---|---|---|
ef91015005 |
|
@ -1,5 +1,5 @@
|
|||
.git/
|
||||
storage/
|
||||
app
|
||||
automation
|
||||
.env
|
||||
tmp/
|
||||
|
|
25
.drone.yml
25
.drone.yml
|
@ -17,25 +17,22 @@ steps:
|
|||
- name: socket
|
||||
path: /var/run/docker.sock
|
||||
environment:
|
||||
MQTT_PASSWORD:
|
||||
from_secret: MQTT_PASSWORD
|
||||
HUE_TOKEN:
|
||||
from_secret: HUE_TOKEN
|
||||
NTFY_TOPIC:
|
||||
from_secret: NTFY_TOPIC
|
||||
GOOGLE_OAUTH_URL:
|
||||
from_secret: GOOGLE_OAUTH_URL
|
||||
GOOGLE_CREDENTIALS:
|
||||
from_secret: GOOGLE_CREDENTIALS
|
||||
MQTT_HOST:
|
||||
from_secret: MQTT_HOST
|
||||
MQTT_PORT:
|
||||
from_secret: MQTT_PORT
|
||||
MQTT_USER:
|
||||
from_secret: MQTT_USER
|
||||
MQTT_PASS:
|
||||
from_secret: MQTT_PASS
|
||||
HUE_BRIDGE:
|
||||
from_secret: HUE_BRIDGE
|
||||
commands:
|
||||
- docker stop automation || true
|
||||
|
||||
- docker rm automation || true
|
||||
|
||||
- docker create -e MQTT_PASSWORD=$MQTT_PASSWORD -e HUE_TOKEN=$HUE_TOKEN -e NTFY_TOPIC=$NTFY_TOPIC -e GOOGLE_OAUTH_URL=$GOOGLE_OAUTH_URL -e GOOGLE_CREDENTIALS=$GOOGLE_CREDENTIALS --network mqtt --name automation automation
|
||||
- docker network connect mqtt automation
|
||||
- docker network connect web automation
|
||||
- docker start automation
|
||||
- docker run -e MQTT_HOST=$MQTT_HOST -e MQTT_PORT=$MQTT_PORT -e MQTT_USER=$MQTT_USER -e MQTT_PASS=$MQTT_PASS -e MQTT_CLIENT_ID=$MQTT_CLIENT_ID -e HUE_BRIDGE=$HUE_BRIDGE --network mqtt --name automation -d automation
|
||||
|
||||
when:
|
||||
branch:
|
||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,4 +1,4 @@
|
|||
storage/
|
||||
app
|
||||
automation
|
||||
.env
|
||||
tmp/
|
||||
|
|
|
@ -7,13 +7,12 @@ RUN go mod download
|
|||
|
||||
COPY . .
|
||||
|
||||
RUN go build -o app
|
||||
RUN go build
|
||||
|
||||
|
||||
FROM golang:alpine
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=build-automation /src/app /app/app
|
||||
COPY --from=build-automation /src/config.yml /app/config.yml
|
||||
COPY --from=build-automation /src/automation /app/automation
|
||||
|
||||
CMD ["/app/app"]
|
||||
CMD ["/app/automation"]
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
package automation
|
||||
|
||||
import (
|
||||
"automation/home"
|
||||
"automation/integration/hue"
|
||||
"automation/integration/ntfy"
|
||||
"automation/presence"
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
paho "github.com/eclipse/paho.mqtt.golang"
|
||||
)
|
||||
|
||||
func on[M any](client paho.Client, topic string, onMessage func(message M)) {
|
||||
var handler paho.MessageHandler = func(c paho.Client, m paho.Message) {
|
||||
if len(m.Payload()) == 0 {
|
||||
// In this case we clear the persistent message
|
||||
// @TODO Maybe implement onClear as a callback? (Currently not needed)
|
||||
return
|
||||
}
|
||||
|
||||
var message M;
|
||||
err := json.Unmarshal(m.Payload(), &message)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
if onMessage != nil {
|
||||
onMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
if token := client.Subscribe(topic, 1, handler); token.Wait() && token.Error() != nil {
|
||||
log.Println(token.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func RegisterAutomations(client paho.Client, prefix string, hue *hue.Hue, notify *ntfy.Notify, home *home.Home, presence *presence.Presence) {
|
||||
presenceAutomation(client, hue, notify, home)
|
||||
mixerAutomation(client, prefix, home)
|
||||
kettleAutomation(client, prefix, home)
|
||||
darknessAutomation(client, hue)
|
||||
frontdoorAutomation(client, prefix, presence)
|
||||
zeusAutomation(client, home)
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
package automation
|
||||
|
||||
import (
|
||||
"automation/integration/hue"
|
||||
"automation/integration/zigbee"
|
||||
|
||||
paho "github.com/eclipse/paho.mqtt.golang"
|
||||
)
|
||||
|
||||
func darknessAutomation(client paho.Client, hue *hue.Hue) {
|
||||
on(client, "automation/darkness/living", func(message zigbee.DarknessPayload) {
|
||||
hue.SetFlag(43, message.IsDark)
|
||||
})
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
package automation
|
||||
|
||||
import (
|
||||
"automation/presence"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
paho "github.com/eclipse/paho.mqtt.golang"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Contact bool `json:"contact"`
|
||||
}
|
||||
|
||||
func frontdoorAutomation(client paho.Client, prefix string, p *presence.Presence) {
|
||||
const length = 15 * time.Minute
|
||||
|
||||
timer := time.NewTimer(length)
|
||||
timer.Stop()
|
||||
|
||||
on(client, fmt.Sprintf("%s/hallway/frontdoor", prefix), func(message Message) {
|
||||
// Always reset the timer if the door is opened
|
||||
if !message.Contact {
|
||||
timer.Reset(length)
|
||||
}
|
||||
|
||||
// If the door opens an there is no one home
|
||||
if !message.Contact && !p.Current() {
|
||||
payload, err := json.Marshal(presence.Message{
|
||||
State: true,
|
||||
Updated: time.Now().UnixMilli(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
token := client.Publish("automation/presence/frontdoor", 1, false, payload)
|
||||
if token.Wait() && token.Error() != nil {
|
||||
log.Println(token.Error())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
go func() {
|
||||
for {
|
||||
<-timer.C
|
||||
// Clear out the value
|
||||
token := client.Publish("automation/presence/frontdoor", 1, false, "")
|
||||
if token.Wait() && token.Error() != nil {
|
||||
log.Println(token.Error())
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
package automation
|
||||
|
||||
import (
|
||||
"automation/device"
|
||||
"automation/home"
|
||||
"automation/integration/zigbee"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
paho "github.com/eclipse/paho.mqtt.golang"
|
||||
)
|
||||
|
||||
func kettleAutomation(client paho.Client, prefix string, home *home.Home) {
|
||||
const name = "kitchen/kettle"
|
||||
const length = 5 * time.Minute
|
||||
|
||||
timer := time.NewTimer(length)
|
||||
timer.Stop()
|
||||
|
||||
on(client, fmt.Sprintf("%s/%s", prefix, name), func(message zigbee.OnOffState) {
|
||||
if message.State {
|
||||
timer.Reset(length)
|
||||
} else {
|
||||
timer.Stop()
|
||||
}
|
||||
})
|
||||
|
||||
go func() {
|
||||
for {
|
||||
<-timer.C
|
||||
log.Println("Turning kettle automatically off")
|
||||
kettle, err := device.GetDevice[device.OnOff](&home.Devices, name)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
break
|
||||
}
|
||||
|
||||
kettle.SetOnOff(false)
|
||||
}
|
||||
}()
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
package automation
|
||||
|
||||
import (
|
||||
"automation/device"
|
||||
"automation/home"
|
||||
"automation/integration/zigbee"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
paho "github.com/eclipse/paho.mqtt.golang"
|
||||
)
|
||||
|
||||
func mixerAutomation(client paho.Client, prefix string, home *home.Home) {
|
||||
on(client, fmt.Sprintf("%s/living/remote", prefix), func(message zigbee.RemoteState) {
|
||||
mixer, err := device.GetDevice[device.OnOff](&home.Devices, "living_room/mixer")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
speakers, err := device.GetDevice[device.OnOff](&home.Devices, "living_room/speakers")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
if message.Action == zigbee.ACTION_ON {
|
||||
if mixer.GetOnOff() {
|
||||
mixer.SetOnOff(false)
|
||||
speakers.SetOnOff(false)
|
||||
} else {
|
||||
mixer.SetOnOff(true)
|
||||
}
|
||||
} else if message.Action == zigbee.ACTION_BRIGHTNESS_UP {
|
||||
if speakers.GetOnOff() {
|
||||
speakers.SetOnOff(false)
|
||||
} else {
|
||||
speakers.SetOnOff(true)
|
||||
mixer.SetOnOff(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
package automation
|
||||
|
||||
import (
|
||||
"automation/device"
|
||||
"automation/home"
|
||||
"automation/integration/hue"
|
||||
"automation/integration/ntfy"
|
||||
"automation/presence"
|
||||
"log"
|
||||
|
||||
paho "github.com/eclipse/paho.mqtt.golang"
|
||||
)
|
||||
|
||||
func presenceAutomation(client paho.Client, hue *hue.Hue, notify *ntfy.Notify, home *home.Home) {
|
||||
on(client, "automation/presence", func(message presence.Message) {
|
||||
log.Printf("Presence changed: %t\n", message.State)
|
||||
|
||||
// Set presence on the hue bridge
|
||||
hue.SetFlag(41, message.State)
|
||||
|
||||
if !message.State {
|
||||
log.Println("Turn off all the devices")
|
||||
|
||||
// Turn off all devices
|
||||
// @TODO Maybe allow for exceptions, could be a list in the config that we check against?
|
||||
for _, dev := range home.Devices {
|
||||
switch d := dev.(type) {
|
||||
case device.OnOff:
|
||||
d.SetOnOff(false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// @TODO Turn off nest thermostat
|
||||
} else {
|
||||
// @TODO Turn on the nest thermostat again
|
||||
}
|
||||
|
||||
// Notify users of presence update
|
||||
notify.Presence(message.State)
|
||||
})
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package automation
|
||||
|
||||
import (
|
||||
"automation/device"
|
||||
"automation/home"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
paho "github.com/eclipse/paho.mqtt.golang"
|
||||
)
|
||||
|
||||
func zeusAutomation(client paho.Client, home *home.Home) {
|
||||
const name = "living_room/zeus"
|
||||
on(client, fmt.Sprintf("automation/appliance/%s", name), func(message struct{Activate bool `json:"activate"`}) {
|
||||
computer, err := device.GetDevice[device.Activate](&home.Devices, name)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
computer.Activate(message.Activate)
|
||||
})
|
||||
}
|
25
config.yml
25
config.yml
|
@ -1,25 +0,0 @@
|
|||
hue:
|
||||
ip: 10.0.0.146
|
||||
|
||||
mqtt:
|
||||
host: mosquitto
|
||||
port: 8883
|
||||
username: mqtt
|
||||
client_id: automation
|
||||
|
||||
zigbee:
|
||||
prefix: zigbee2mqtt
|
||||
|
||||
kasa:
|
||||
outlets:
|
||||
living_room/mixer: 10.0.0.49
|
||||
living_room/speakers: 10.0.0.182
|
||||
|
||||
computers:
|
||||
living_room/zeus:
|
||||
mac: 30:9c:23:60:9c:13
|
||||
room: Living Room
|
||||
url: http://10.0.0.2:9000/start-pc?mac=30:9c:23:60:9c:13
|
||||
|
||||
google:
|
||||
username: Dreaded_X
|
|
@ -1,83 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"automation/device"
|
||||
"encoding/base64"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
Hue struct {
|
||||
Token string `yaml:"token" envconfig:"HUE_TOKEN"`
|
||||
IP string `yaml:"ip" envconfig:"HUE_IP"`
|
||||
} `yaml:"hue"`
|
||||
|
||||
Ntfy struct {
|
||||
Topic string `yaml:"topic" envconfig:"NTFY_TOPIC"`
|
||||
} `yaml:"ntfy"`
|
||||
|
||||
MQTT struct {
|
||||
Host string `yaml:"host" envconfig:"MQTT_HOST"`
|
||||
Port int `yaml:"port" envconfig:"MQTT_PORT"`
|
||||
Username string `yaml:"username" envconfig:"MQTT_USERNAME"`
|
||||
Password string `yaml:"password" envconfig:"MQTT_PASSWORD"`
|
||||
ClientID string `yaml:"client_id" envconfig:"MQTT_CLIENT_ID"`
|
||||
} `yaml:"mqtt"`
|
||||
|
||||
Zigbee struct {
|
||||
MQTTPrefix string `yaml:"prefix" envconfig:"ZIGBEE2MQTT_PREFIX"`
|
||||
}
|
||||
|
||||
Kasa struct {
|
||||
Outlets map[device.InternalName]string `yaml:"outlets"`
|
||||
} `yaml:"kasa"`
|
||||
|
||||
Computer map[device.InternalName]struct {
|
||||
MACAddress string `yaml:"mac"`
|
||||
Url string `yaml:"url"`
|
||||
} `yaml:"computers"`
|
||||
|
||||
Google struct {
|
||||
Username string `yaml:"username" envconfig:"GOOGLE_USERNAME"`
|
||||
OAuthUrl string `yaml:"oauth_url" envconfig:"GOOGLE_OAUTH_URL"`
|
||||
Credentials Credentials `yaml:"credentials" envconfig:"GOOGLE_CREDENTIALS"`
|
||||
} `yaml:"google"`
|
||||
}
|
||||
|
||||
type Credentials []byte
|
||||
|
||||
func (c *Credentials) Decode(value string) error {
|
||||
b, err := base64.StdEncoding.DecodeString(value)
|
||||
*c = b
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func Get() config {
|
||||
// First load the config from the yaml file
|
||||
f, err := os.Open("config.yml")
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to open config file", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var cfg config
|
||||
decoder := yaml.NewDecoder(f)
|
||||
err = decoder.Decode(&cfg)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to parse config file", err)
|
||||
}
|
||||
|
||||
// Then load values from environment
|
||||
// This can be used to either override the config or pass in secrets
|
||||
err = envconfig.Process("", &cfg)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to parse environmet config", err)
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
package device
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Basic interface {
|
||||
GetID() InternalName
|
||||
}
|
||||
|
||||
type OnOff interface {
|
||||
SetOnOff(state bool)
|
||||
GetOnOff() bool
|
||||
}
|
||||
|
||||
type Activate interface {
|
||||
Activate(state bool)
|
||||
}
|
||||
|
||||
func GetDevices[K any](devices *map[InternalName]Basic) map[InternalName]K {
|
||||
devs := make(map[InternalName]K)
|
||||
|
||||
for name, device := range *devices {
|
||||
if dev, ok := device.(K); ok {
|
||||
devs[name] = dev
|
||||
}
|
||||
}
|
||||
|
||||
return devs
|
||||
}
|
||||
|
||||
func GetDevice[K any](devices *map[InternalName]Basic, name InternalName) (K, error) {
|
||||
d, ok := (*devices)[name]
|
||||
if !ok {
|
||||
var noop K
|
||||
return noop, fmt.Errorf("Device '%s' does not exist", name)
|
||||
}
|
||||
|
||||
dev, ok := d.(K)
|
||||
if !ok {
|
||||
var noop K
|
||||
return noop, fmt.Errorf("Device '%s' is not the expected type", name)
|
||||
}
|
||||
|
||||
return dev, nil
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
package device
|
||||
|
||||
import "strings"
|
||||
|
||||
type InternalName string
|
||||
|
||||
func (n InternalName) Room() string {
|
||||
s := strings.Split(string(n), "/")
|
||||
room := ""
|
||||
if len(s) > 1 {
|
||||
room = s[0]
|
||||
}
|
||||
room = strings.ReplaceAll(room, "_", " ")
|
||||
|
||||
return room
|
||||
}
|
||||
|
||||
func (n InternalName) Name() string {
|
||||
s := strings.Split(string(n), "/")
|
||||
name := s[0]
|
||||
if len(s) > 1 {
|
||||
name = s[1]
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
func (n InternalName) String() string {
|
||||
return string(n)
|
||||
}
|
||||
|
31
go.mod
31
go.mod
|
@ -1,39 +1,16 @@
|
|||
module automation
|
||||
|
||||
go 1.19
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/eclipse/paho.mqtt.golang v1.3.5
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/jellydator/ttlcache/v3 v3.0.0
|
||||
github.com/joho/godotenv v1.4.0
|
||||
github.com/kelseyhightower/envconfig v1.4.0
|
||||
github.com/kr/pretty v0.3.1
|
||||
github.com/r3labs/sse/v2 v2.8.1
|
||||
google.golang.org/api v0.103.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
github.com/kelvins/sunrisesunset v0.0.0-20210220141756-39fa1bd816d5
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute v1.12.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.1 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.7.0 // indirect
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.9.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
|
||||
golang.org/x/text v0.4.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c // indirect
|
||||
google.golang.org/grpc v1.50.1 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
github.com/r3labs/sse/v2 v2.8.1 // indirect
|
||||
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 // indirect
|
||||
gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect
|
||||
)
|
||||
|
|
153
go.sum
153
go.sum
|
@ -1,172 +1,25 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y=
|
||||
cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0=
|
||||
cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
|
||||
cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48=
|
||||
cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
|
||||
cloud.google.com/go/longrunning v0.1.1 h1:y50CXG4j0+qvEukslYFBCrzaXX0qpFbBzc3PchSu/LE=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.0 h1:y8Yozv7SZtlU//QXbezB6QkpuE6jMD2/gfzk4AftXjs=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
|
||||
github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ=
|
||||
github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
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/jellydator/ttlcache/v3 v3.0.0 h1:zmFhqrB/4sKiEiJHhtseJsNRE32IMVmJSs4++4gaQO4=
|
||||
github.com/jellydator/ttlcache/v3 v3.0.0/go.mod h1:WwTaEmcXQ3MTjOm4bsZoDFiCu/hMvNWLO1w67RXz6h4=
|
||||
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/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/kelvins/sunrisesunset v0.0.0-20210220141756-39fa1bd816d5 h1:ouekCqYkMw4QXFCaLyYqjBe99/MUW4Qf3DJhCRh1G18=
|
||||
github.com/kelvins/sunrisesunset v0.0.0-20210220141756-39fa1bd816d5/go.mod h1:3oZ7G+fb8Z8KF+KPHxeDO3GWpEjgvk/f+d/yaxmDRT4=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
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 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U=
|
||||
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b h1:tvrvnPFcdzp294diPnrdZZZ8XUt2Tyj7svb7X52iDuU=
|
||||
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk=
|
||||
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
||||
google.golang.org/api v0.103.0 h1:9yuVqlu2JCvcLg9p8S3fcFLZij8EPSyvODIY1rkMizQ=
|
||||
google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c h1:QgY/XxIAIeccR+Ca/rDdKubLIU9rcJ3xfy1DC/Wd2Oo=
|
||||
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY=
|
||||
google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
|
96
home/home.go
96
home/home.go
|
@ -1,96 +0,0 @@
|
|||
package home
|
||||
|
||||
import (
|
||||
"automation/config"
|
||||
"automation/device"
|
||||
"automation/integration/google"
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"google.golang.org/api/homegraph/v1"
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
type Home struct {
|
||||
Service *google.Service
|
||||
Username string
|
||||
|
||||
Devices map[device.InternalName]device.Basic
|
||||
}
|
||||
|
||||
// Auto populate and update the device list
|
||||
func New(username string, credentials config.Credentials, oauthUrl string) *Home {
|
||||
home := &Home{Username: username, Devices: make(map[device.InternalName]device.Basic)}
|
||||
|
||||
homegraphService, err := homegraph.NewService(context.Background(), option.WithCredentialsJSON(credentials))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
home.Service = google.NewService(home, homegraphService, oauthUrl)
|
||||
|
||||
return home
|
||||
}
|
||||
|
||||
func (h *Home) AddDevice(d device.Basic) {
|
||||
h.Devices[d.GetID()] = d
|
||||
|
||||
log.Printf("Added %s in %s (%s)\n", d.GetID().Name(), d.GetID().Room(), d.GetID())
|
||||
}
|
||||
|
||||
func (h *Home) Sync(_ context.Context, _ string) ([]*google.Device, error) {
|
||||
var devices []*google.Device
|
||||
|
||||
for _, device := range device.GetDevices[google.DeviceInterface](&h.Devices) {
|
||||
devices = append(devices, device.Sync())
|
||||
}
|
||||
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
func (h *Home) Query(_ context.Context, _ string, handles []google.DeviceHandle) (map[string]google.DeviceState, error) {
|
||||
states := make(map[string]google.DeviceState)
|
||||
|
||||
for _, handle := range handles {
|
||||
if device, err := device.GetDevice[google.DeviceInterface](&h.Devices, device.InternalName(handle.ID)); err == nil {
|
||||
states[device.GetID().String()] = device.Query()
|
||||
} else {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
return states, nil
|
||||
}
|
||||
|
||||
func (h *Home) Execute(_ context.Context, _ string, commands []google.Command) (*google.ExecuteResponse, error) {
|
||||
resp := &google.ExecuteResponse{
|
||||
UpdatedState: google.NewDeviceState(true),
|
||||
FailedDevices: make(map[string]struct{ Devices []string }),
|
||||
}
|
||||
|
||||
for _, command := range commands {
|
||||
for _, execution := range command.Execution {
|
||||
for _, handle := range command.Devices {
|
||||
if device, err := device.GetDevice[google.DeviceInterface](&h.Devices, device.InternalName(handle.ID)); err == nil {
|
||||
errCode, online := device.Execute(execution, &resp.UpdatedState)
|
||||
|
||||
// Update the state
|
||||
h.Devices[device.GetID()] = device
|
||||
if !online {
|
||||
resp.OfflineDevices = append(resp.OfflineDevices, handle.ID)
|
||||
} else if len(errCode) == 0 {
|
||||
resp.UpdatedDevices = append(resp.UpdatedDevices, handle.ID)
|
||||
} else {
|
||||
e := resp.FailedDevices[errCode]
|
||||
e.Devices = append(e.Devices, handle.ID)
|
||||
resp.FailedDevices[errCode] = e
|
||||
}
|
||||
} else {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
package google
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type CommandName string
|
||||
|
||||
type Execution struct {
|
||||
Name CommandName
|
||||
|
||||
OnOff *CommandOnOffData
|
||||
StartStop *CommandStartStopData
|
||||
GetCameraStream *CommandGetCameraStreamData
|
||||
ActivateScene *CommandActivateSceneData
|
||||
}
|
||||
|
||||
func (c *Execution) UnmarshalJSON(data []byte) error {
|
||||
var tmp struct {
|
||||
Name CommandName `json:"command"`
|
||||
Params json.RawMessage `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
err := json.Unmarshal(data, &tmp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Name = tmp.Name
|
||||
|
||||
var details interface{}
|
||||
switch c.Name {
|
||||
case CommandOnOff:
|
||||
c.OnOff = &CommandOnOffData{}
|
||||
details = c.OnOff
|
||||
|
||||
case CommandStartStop:
|
||||
c.StartStop = &CommandStartStopData{}
|
||||
details = c.StartStop
|
||||
|
||||
case CommandGetCameraStream:
|
||||
c.GetCameraStream = &CommandGetCameraStreamData{}
|
||||
details = c.GetCameraStream
|
||||
|
||||
case CommandActivateScene:
|
||||
c.ActivateScene = &CommandActivateSceneData{}
|
||||
details = c.ActivateScene
|
||||
|
||||
default:
|
||||
return fmt.Errorf("Command (%s) is not implemented", c.Name)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(tmp.Params, details)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// https://developers.google.com/assistant/smarthome/traits/onoff
|
||||
const CommandOnOff CommandName = "action.devices.commands.OnOff"
|
||||
|
||||
type CommandOnOffData struct {
|
||||
On bool `json:"on"`
|
||||
}
|
||||
|
||||
// https://developers.google.com/assistant/smarthome/traits/startstop
|
||||
const CommandStartStop CommandName = "action.devices.commands.StartStop"
|
||||
|
||||
type CommandStartStopData struct {
|
||||
Start bool `json:"start"`
|
||||
Zone string `json:"zone,omitempty"`
|
||||
MultipleZones []string `json:"multipleZones,omitempty"`
|
||||
}
|
||||
|
||||
// https://developers.google.com/assistant/smarthome/traits/camerastream
|
||||
const CommandGetCameraStream CommandName = "action.devices.commands.GetCameraStream"
|
||||
|
||||
type CommandGetCameraStreamData struct {
|
||||
StreamToChromecast bool `json:"StreamToChromecast"`
|
||||
SupportedStreamProtocols []string `json:"SupportedStreamProtocols"`
|
||||
}
|
||||
|
||||
// https://developers.google.com/assistant/smarthome/traits/scene
|
||||
const CommandActivateScene CommandName = "action.devices.commands.ActivateScene"
|
||||
|
||||
type CommandActivateSceneData struct {
|
||||
Deactivate bool `json:"deactivate"`
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
package google
|
||||
|
||||
type DeviceName struct {
|
||||
DefaultNames []string `json:"defaultNames,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Nicknames []string `json:"nicknames,omitempty"`
|
||||
}
|
||||
|
||||
type DeviceInfo struct {
|
||||
Manufacturer string `json:"manufacturer,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
HwVersion string `json:"hwVersion,omitempty"`
|
||||
SwVersion string `json:"swVersion,omitempty"`
|
||||
}
|
||||
|
||||
type OtherDeviceID struct {
|
||||
AgentID string `json:"agentId,omitempty"`
|
||||
DeviceID string `json:"deviceId,omitempty"`
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
ID string `json:"id"`
|
||||
Type Type `json:"type"`
|
||||
|
||||
Traits []Trait `json:"traits"`
|
||||
|
||||
Name DeviceName `json:"name"`
|
||||
|
||||
WillReportState bool `json:"willReportState"`
|
||||
|
||||
NotificationSupportedByAgent bool `json:"notificationSupportedByAgent,omitempty"`
|
||||
|
||||
RoomHint string `json:"roomHint,omitempty"`
|
||||
|
||||
DeviceInfo DeviceInfo `json:"deviceInfo,omitempty"`
|
||||
|
||||
Attributes map[string]interface{} `json:"attributes,omitempty"`
|
||||
|
||||
CustomData map[string]interface{} `json:"customDate,omitempty"`
|
||||
|
||||
OtherDeviceIDs []OtherDeviceID `json:"otherDeviceIds,omitempty"`
|
||||
}
|
||||
|
||||
func NewDevice(id string, typ Type) *Device {
|
||||
return &Device{
|
||||
ID: id,
|
||||
Type: typ,
|
||||
Attributes: make(map[string]interface{}),
|
||||
CustomData: make(map[string]interface{}),
|
||||
}
|
||||
}
|
|
@ -1,246 +0,0 @@
|
|||
package google
|
||||
|
||||
import (
|
||||
"automation/device"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/jellydator/ttlcache/v3"
|
||||
)
|
||||
|
||||
type DeviceInterface interface {
|
||||
device.Basic
|
||||
|
||||
IsGoogleDevice()
|
||||
Sync() *Device
|
||||
Query() DeviceState
|
||||
Execute(execution Execution, updatedState *DeviceState) (errCode string, online bool)
|
||||
}
|
||||
|
||||
// https://developers.google.com/assistant/smarthome/reference/intent/sync
|
||||
type syncResponse struct {
|
||||
RequestID string `json:"requestId"`
|
||||
Payload struct {
|
||||
UserID string `json:"agentUserId"`
|
||||
ErrorCode string `json:"errorCode,omitempty"`
|
||||
DebugString string `json:"debugString,omitempty"`
|
||||
Devices []*Device `json:"devices"`
|
||||
} `json:"payload"`
|
||||
}
|
||||
|
||||
// https://developers.google.com/assistant/smarthome/reference/intent/query
|
||||
type queryResponse struct {
|
||||
RequestID string `json:"requestId"`
|
||||
Payload struct {
|
||||
ErrorCode string `json:"errorCode,omitempty"`
|
||||
DebugString string `json:"debugString,omitempty"`
|
||||
Devices map[string]DeviceState `json:"devices"`
|
||||
} `json:"payload"`
|
||||
}
|
||||
|
||||
type executeRespPayload struct {
|
||||
IDs []string `json:"ids"`
|
||||
Status Status `json:"status"`
|
||||
ErrorCode string `json:"errorCode,omitempty"`
|
||||
States DeviceState `json:"states,omitempty"`
|
||||
}
|
||||
|
||||
type executeResponse struct {
|
||||
RequestID string `json:"requestId"`
|
||||
Payload struct {
|
||||
ErrorCode string `json:"errorCode,omitempty"`
|
||||
DebugString string `json:"debugString,omitempty"`
|
||||
Commands []executeRespPayload `json:"commands,omitempty"`
|
||||
} `json:"payload"`
|
||||
}
|
||||
|
||||
// @TODO We can also implement this as a cache loader function
|
||||
// Note sure how to report the correct errors in that case?
|
||||
func (s *Service) getUser(authorization string) (string, int) {
|
||||
// @TODO Make oids url configurable
|
||||
|
||||
cached := s.cache.Get(authorization)
|
||||
if cached != nil {
|
||||
return cached.Value(), http.StatusOK
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/userinfo", s.oauthUrl), nil)
|
||||
if err != nil {
|
||||
log.Println("Failed to make request to to login server")
|
||||
return "", http.StatusInternalServerError
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", authorization)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
|
||||
// If we get something other than 200, error out
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Println("Not logged in...")
|
||||
return "", resp.StatusCode
|
||||
}
|
||||
|
||||
// Get the preferred_username from the userinfo
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Println("Failed to read body")
|
||||
return "", resp.StatusCode
|
||||
}
|
||||
|
||||
var body struct {
|
||||
PreferredUsername string `json:"preferred_username"`
|
||||
}
|
||||
|
||||
err = json.Unmarshal(bodyBytes, &body)
|
||||
if err != nil {
|
||||
log.Println("Failed to marshal body")
|
||||
return "", http.StatusInternalServerError
|
||||
}
|
||||
|
||||
if len(body.PreferredUsername) == 0 {
|
||||
log.Println("Received empty username from userinfo endpoint")
|
||||
return "", http.StatusInternalServerError
|
||||
}
|
||||
|
||||
s.cache.Set(authorization, body.PreferredUsername, ttlcache.DefaultTTL)
|
||||
|
||||
return body.PreferredUsername, http.StatusOK
|
||||
}
|
||||
|
||||
func (s *Service) FullfillmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
// Check if we are logged in
|
||||
var userID string
|
||||
if auth, ok := r.Header["Authorization"]; ok && len(auth) > 0 {
|
||||
var statusCode int
|
||||
userID, statusCode = s.getUser(auth[0])
|
||||
if statusCode != http.StatusOK {
|
||||
w.WriteHeader(statusCode)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
log.Println("No authorization provided")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
fullfimentReq := &FullfillmentRequest{}
|
||||
err := json.NewDecoder(r.Body).Decode(&fullfimentReq)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("JSON Deserialization failed"))
|
||||
return
|
||||
}
|
||||
|
||||
if len(fullfimentReq.Inputs) != 1 {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Unsupported number of inputs"))
|
||||
return
|
||||
}
|
||||
|
||||
switch fullfimentReq.Inputs[0].Intent {
|
||||
case IntentSync:
|
||||
devices, err := s.provider.Sync(r.Context(), userID)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
w.Write([]byte("Failed to sync"))
|
||||
}
|
||||
|
||||
syncResp := &syncResponse{
|
||||
RequestID: fullfimentReq.RequestID,
|
||||
}
|
||||
syncResp.Payload.UserID = userID
|
||||
syncResp.Payload.Devices = devices
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
err = json.NewEncoder(w).Encode(syncResp)
|
||||
if err != nil {
|
||||
log.Println("Error serializing", err)
|
||||
}
|
||||
|
||||
case IntentQuery:
|
||||
states, err := s.provider.Query(r.Context(), userID, fullfimentReq.Inputs[0].Query.Devices)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
w.Write([]byte("Failed to sync"))
|
||||
}
|
||||
|
||||
queryResp := &queryResponse{
|
||||
RequestID: fullfimentReq.RequestID,
|
||||
}
|
||||
queryResp.Payload.Devices = states
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
err = json.NewEncoder(w).Encode(queryResp)
|
||||
if err != nil {
|
||||
log.Println("Error serializing", err)
|
||||
}
|
||||
|
||||
case IntentExecute:
|
||||
response, err := s.provider.Execute(r.Context(), userID, fullfimentReq.Inputs[0].Execute.Commands)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
w.Write([]byte("Failed to sync"))
|
||||
}
|
||||
|
||||
executeResp := &executeResponse{
|
||||
RequestID: fullfimentReq.RequestID,
|
||||
}
|
||||
|
||||
if len(response.UpdatedDevices) > 0 {
|
||||
c := executeRespPayload{
|
||||
Status: StatusSuccess,
|
||||
States: response.UpdatedState,
|
||||
}
|
||||
|
||||
for _, id := range response.UpdatedDevices {
|
||||
c.IDs = append(c.IDs, id)
|
||||
}
|
||||
|
||||
executeResp.Payload.Commands = append(executeResp.Payload.Commands, c)
|
||||
}
|
||||
|
||||
if len(response.OfflineDevices) > 0 {
|
||||
c := executeRespPayload{
|
||||
Status: StatusOffline,
|
||||
}
|
||||
|
||||
for _, id := range response.UpdatedDevices {
|
||||
c.IDs = append(c.IDs, id)
|
||||
}
|
||||
|
||||
executeResp.Payload.Commands = append(executeResp.Payload.Commands, c)
|
||||
}
|
||||
|
||||
for errCode, details := range response.FailedDevices {
|
||||
c := executeRespPayload{
|
||||
Status: StatusError,
|
||||
ErrorCode: errCode,
|
||||
}
|
||||
|
||||
for _, id := range details.Devices {
|
||||
c.IDs = append(c.IDs, id)
|
||||
}
|
||||
|
||||
executeResp.Payload.Commands = append(executeResp.Payload.Commands, c)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
err = json.NewEncoder(w).Encode(executeResp)
|
||||
if err != nil {
|
||||
log.Println("Error serializing", err)
|
||||
}
|
||||
|
||||
default:
|
||||
log.Println("Intent is not implemented")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Not implemented for now"))
|
||||
}
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
package google
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type Intent string
|
||||
|
||||
const (
|
||||
IntentSync Intent = "action.devices.SYNC"
|
||||
IntentQuery = "action.devices.QUERY"
|
||||
IntentExecute = "action.devices.EXECUTE"
|
||||
)
|
||||
|
||||
type DeviceHandle struct {
|
||||
ID string `json:"id"`
|
||||
|
||||
CustomData map[string]interface{} `json:"customData,omitempty"`
|
||||
}
|
||||
|
||||
type queryPayload struct {
|
||||
Devices []DeviceHandle `json:"devices"`
|
||||
}
|
||||
|
||||
type Command struct {
|
||||
Devices []DeviceHandle `json:"devices"`
|
||||
Execution []Execution `json:"execution"`
|
||||
}
|
||||
|
||||
type executePayload struct {
|
||||
Commands []Command `json:"commands"`
|
||||
}
|
||||
|
||||
type fullfilmentInput struct {
|
||||
Intent Intent
|
||||
|
||||
Query *queryPayload
|
||||
Execute *executePayload
|
||||
}
|
||||
|
||||
type FullfillmentRequest struct {
|
||||
RequestID string `json:"requestId"`
|
||||
Inputs []fullfilmentInput `json:"inputs"`
|
||||
}
|
||||
|
||||
func (i *fullfilmentInput) UnmarshalJSON(data []byte) error {
|
||||
var tmp struct {
|
||||
Intent Intent `json:"intent"`
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
}
|
||||
|
||||
err := json.Unmarshal(data, &tmp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i.Intent = tmp.Intent
|
||||
switch i.Intent {
|
||||
case IntentQuery:
|
||||
payload := &queryPayload{}
|
||||
err = json.Unmarshal(tmp.Payload, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i.Query = payload
|
||||
|
||||
case IntentExecute:
|
||||
payload := &executePayload{}
|
||||
err = json.Unmarshal(tmp.Payload, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i.Execute = payload
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
package google
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jellydator/ttlcache/v3"
|
||||
"google.golang.org/api/homegraph/v1"
|
||||
)
|
||||
|
||||
type ExecuteResponse struct {
|
||||
UpdatedState DeviceState
|
||||
UpdatedDevices []string
|
||||
OfflineDevices []string
|
||||
// The key is the errorCode that is associated with the devices
|
||||
FailedDevices map[string]struct {
|
||||
Devices []string
|
||||
}
|
||||
}
|
||||
|
||||
type Provider interface {
|
||||
Sync(context.Context, string) ([]*Device, error)
|
||||
Query(context.Context, string, []DeviceHandle) (map[string]DeviceState, error)
|
||||
Execute(context.Context, string, []Command) (*ExecuteResponse, error)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
provider Provider
|
||||
deviceService *homegraph.DevicesService
|
||||
oauthUrl string
|
||||
|
||||
cache *ttlcache.Cache[string, string]
|
||||
}
|
||||
|
||||
func NewService(provider Provider, service *homegraph.Service, oauthUrl string) *Service {
|
||||
s := Service{
|
||||
provider: provider,
|
||||
deviceService: homegraph.NewDevicesService(service),
|
||||
oauthUrl: oauthUrl,
|
||||
cache: ttlcache.New(
|
||||
ttlcache.WithTTL[string, string](30 * time.Minute),
|
||||
),
|
||||
}
|
||||
|
||||
go s.cache.Start()
|
||||
|
||||
return &s
|
||||
}
|
||||
|
||||
func (s *Service) RequestSync(ctx context.Context, userID string) error {
|
||||
call := s.deviceService.RequestSync(&homegraph.RequestSyncDevicesRequest{
|
||||
AgentUserId: userID,
|
||||
})
|
||||
|
||||
call.Context(ctx)
|
||||
resp, err := call.Do()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.ServerResponse.HTTPStatusCode != http.StatusOK {
|
||||
return errors.New(fmt.Sprintf("sync failed: %d", resp.ServerResponse.HTTPStatusCode))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) ReportState(ctx context.Context, userID string, states map[string]DeviceState) error {
|
||||
j, err := json.Marshal(states)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
call := s.deviceService.ReportStateAndNotification(&homegraph.ReportStateAndNotificationRequest{
|
||||
AgentUserId: userID,
|
||||
EventId: uuid.New().String(),
|
||||
RequestId: uuid.New().String(),
|
||||
Payload: &homegraph.StateAndNotificationPayload{
|
||||
Devices: &homegraph.ReportStateAndNotificationDevice{
|
||||
States: j,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
call.Context(ctx)
|
||||
resp, err := call.Do()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.ServerResponse.HTTPStatusCode != http.StatusOK {
|
||||
return errors.New(fmt.Sprintf("report failed: %d", resp.ServerResponse.HTTPStatusCode))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
package google
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type DeviceState struct {
|
||||
Online bool
|
||||
Status Status
|
||||
|
||||
state map[string]interface{}
|
||||
}
|
||||
|
||||
func (ds DeviceState) MarshalJSON() ([]byte, error) {
|
||||
payload := make(map[string]interface{})
|
||||
|
||||
payload["online"] = ds.Online
|
||||
if len(ds.Status) > 0 {
|
||||
payload["status"] = ds.Status
|
||||
}
|
||||
|
||||
for k, v := range ds.state {
|
||||
payload[k] = v
|
||||
}
|
||||
|
||||
return json.Marshal(payload)
|
||||
}
|
||||
|
||||
func NewDeviceState(online bool) DeviceState {
|
||||
return DeviceState{
|
||||
Online: online,
|
||||
state: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// https://developers.google.com/assistant/smarthome/traits/onoff
|
||||
func (ds DeviceState) RecordOnOff(on bool) DeviceState {
|
||||
ds.state["on"] = on
|
||||
|
||||
return ds
|
||||
}
|
||||
|
||||
// https://developers.google.com/assistant/smarthome/traits/runcycle
|
||||
func (ds DeviceState) RecordRunCycle(state int) DeviceState {
|
||||
if state == 0 {
|
||||
} else if state == 1 {
|
||||
ds.state["currentRunCycle"] = []struct{
|
||||
CurrentCycle string `json:"currentCycle"`
|
||||
Lang string `json:"lang"`
|
||||
}{
|
||||
{
|
||||
CurrentCycle: "Wash",
|
||||
Lang: "en",
|
||||
},
|
||||
}
|
||||
} else if state == 2 {
|
||||
ds.state["currentTotalRemainingTime"] = 0
|
||||
}
|
||||
|
||||
return ds
|
||||
}
|
||||
|
||||
// https://developers.google.com/assistant/smarthome/traits/startstop
|
||||
func (ds DeviceState) RecordStartStop(running bool, paused ...bool) DeviceState {
|
||||
ds.state["isRunning"] = running
|
||||
if len(paused) > 0 {
|
||||
ds.state["isPaused"] = paused[0]
|
||||
}
|
||||
|
||||
return ds
|
||||
}
|
||||
|
||||
// https://developers.google.com/assistant/smarthome/traits/camerastream
|
||||
func (ds DeviceState) RecordCameraStream(url string) DeviceState {
|
||||
ds.state["cameraStreamProtocol"] = "progressive_mp4"
|
||||
ds.state["cameraStreamAccessUrl"] = url
|
||||
|
||||
return ds
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
package google
|
||||
|
||||
type Status string
|
||||
|
||||
const (
|
||||
StatusSuccess Status = "SUCCESS"
|
||||
StatusOffline = "OFFLINE"
|
||||
StatusException = "EXCEPTIONS"
|
||||
StatusError = "ERROR"
|
||||
)
|
|
@ -1,72 +0,0 @@
|
|||
package google
|
||||
|
||||
import "github.com/kr/pretty"
|
||||
|
||||
type Trait string
|
||||
|
||||
// https://developers.google.com/assistant/smarthome/traits/onoff
|
||||
const TraitOnOff Trait = "action.devices.traits.OnOff"
|
||||
|
||||
func (d *Device) AddOnOffTrait(onlyCommand bool, onlyQuery bool) *Device {
|
||||
d.Traits = append(d.Traits, TraitOnOff)
|
||||
if onlyCommand {
|
||||
d.Attributes["commandOnlyOnOff"] = true
|
||||
}
|
||||
if onlyQuery {
|
||||
d.Attributes["queryOnlyOnOff"] = true
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// https://developers.google.com/assistant/smarthome/traits/startstop
|
||||
const TraitStartStop = "action.devices.traits.StartStop"
|
||||
|
||||
func (d *Device) AddStartStopTrait(pausable bool) *Device {
|
||||
d.Traits = append(d.Traits, TraitStartStop)
|
||||
|
||||
if pausable {
|
||||
d.Attributes["pausable"] = true
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// https://developers.google.com/assistant/smarthome/traits/onoff
|
||||
const TraitRunCycle = "action.devices.traits.RunCycle"
|
||||
|
||||
func (d *Device) AddRunCycleTrait() *Device {
|
||||
d.Traits = append(d.Traits, TraitRunCycle)
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// https://developers.google.com/assistant/smarthome/traits/camerastream
|
||||
const TraitCameraStream = "action.devices.traits.CameraStream"
|
||||
|
||||
func (d *Device) AddCameraStreamTrait(authTokenNeeded bool, supportedProtocols ...string) *Device {
|
||||
d.Traits = append(d.Traits, TraitCameraStream)
|
||||
|
||||
if len(supportedProtocols) > 0 {
|
||||
d.Attributes["cameraStreamSupportedProtocols"] = supportedProtocols
|
||||
}
|
||||
|
||||
d.Attributes["cameraStreamNeedAuthToken"] = authTokenNeeded
|
||||
|
||||
pretty.Logln(d)
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// https://developers.google.com/assistant/smarthome/traits/scene
|
||||
const TraitScene = "action.devices.traits.Scene"
|
||||
|
||||
func (d *Device) AddSceneTrait(reversible bool) *Device {
|
||||
d.Traits = append(d.Traits, TraitScene)
|
||||
|
||||
if reversible {
|
||||
d.Attributes["sceneReversible"] = true
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package google
|
||||
|
||||
type Type string
|
||||
|
||||
// https://developers.google.com/assistant/smarthome/guides
|
||||
const (
|
||||
TypeKettle = "action.devices.types.KETTLE"
|
||||
TypeScene = "action.devices.types.SCENE"
|
||||
)
|
|
@ -1,60 +0,0 @@
|
|||
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"`
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
package hue
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"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 New(ip string, token string) *Hue {
|
||||
hue := Hue{ip: ip, login: token, 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
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
package kasa
|
||||
|
||||
import (
|
||||
"automation/device"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
// This implementation is based on:
|
||||
// https://www.softscheck.com/en/blog/tp-link-reverse-engineering/
|
||||
|
||||
type Device interface {
|
||||
device.Basic
|
||||
|
||||
IsKasaDevice()
|
||||
GetIP() string
|
||||
}
|
||||
|
||||
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) ([]byte, error) {
|
||||
var key byte = 171
|
||||
|
||||
if len(data) < 4 {
|
||||
return nil, fmt.Errorf("Array has a minumun size of 4")
|
||||
}
|
||||
|
||||
size := binary.BigEndian.Uint32(data[0:4])
|
||||
buf := make([]byte, size)
|
||||
|
||||
for i := 0; i < int(size); i++ {
|
||||
a := key ^ data[i+4]
|
||||
key = data[i+4]
|
||||
buf[i] = a
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func sendCmd(kasa Device, cmd cmd) (reply, error) {
|
||||
con, err := net.Dial("tcp", fmt.Sprintf("%s:9999", kasa.GetIP()))
|
||||
if err != nil {
|
||||
return reply{}, err
|
||||
}
|
||||
|
||||
defer con.Close()
|
||||
|
||||
b, err := json.Marshal(cmd)
|
||||
if err != nil {
|
||||
return reply{}, err
|
||||
}
|
||||
|
||||
_, err = con.Write(encrypt(b))
|
||||
if err != nil {
|
||||
return reply{}, err
|
||||
}
|
||||
|
||||
resp := make([]byte, 2048)
|
||||
_, err = con.Read(resp)
|
||||
if err != nil {
|
||||
return reply{}, err
|
||||
}
|
||||
|
||||
d, err := decrypt(resp)
|
||||
if err != nil {
|
||||
return reply{}, err
|
||||
}
|
||||
|
||||
var reply reply
|
||||
err = json.Unmarshal(d, &reply)
|
||||
if err != nil {
|
||||
return reply, err
|
||||
}
|
||||
|
||||
return reply, err
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
package kasa
|
||||
|
||||
type reply struct {
|
||||
System struct {
|
||||
SetRelayState struct {
|
||||
ErrCode int `json:"err_code"`
|
||||
} `json:"set_relay_state"`
|
||||
GetSysinfo struct {
|
||||
RelayState int `json:"relay_state"`
|
||||
ErrCode int `json:"err_code"`
|
||||
} `json:"get_sysinfo"`
|
||||
} `json:"system"`
|
||||
}
|
||||
|
||||
type SetRelayState struct {
|
||||
State int `json:"state"`
|
||||
}
|
||||
|
||||
type GetSysinfo struct{}
|
||||
|
||||
type cmd struct {
|
||||
System struct {
|
||||
SetRelayState *SetRelayState `json:"set_relay_state,omitempty"`
|
||||
GetSysinfo *GetSysinfo `json:"get_sysinfo,omitempty"`
|
||||
} `json:"system"`
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
package kasa
|
||||
|
||||
import (
|
||||
"automation/device"
|
||||
"log"
|
||||
)
|
||||
|
||||
type Outlet struct {
|
||||
name device.InternalName
|
||||
ip string
|
||||
}
|
||||
|
||||
|
||||
func NewOutlet(name device.InternalName, ip string) *Outlet {
|
||||
return &Outlet{name, ip}
|
||||
}
|
||||
|
||||
// kasa.Device
|
||||
var _ Device = (*Outlet)(nil)
|
||||
func (*Outlet) IsKasaDevice() {}
|
||||
|
||||
// kasa.Device
|
||||
func (o *Outlet) GetIP() string {
|
||||
return o.ip
|
||||
}
|
||||
|
||||
// device.Basic
|
||||
var _ device.Basic = (*Outlet)(nil)
|
||||
func (o *Outlet) GetID() device.InternalName {
|
||||
return o.name
|
||||
}
|
||||
|
||||
// device.OnOff
|
||||
var _ device.OnOff = (*Outlet)(nil)
|
||||
func (o *Outlet) SetOnOff(on bool) {
|
||||
var cmd cmd
|
||||
cmd.System.SetRelayState = &SetRelayState{State: 0}
|
||||
if on {
|
||||
cmd.System.SetRelayState.State = 1
|
||||
}
|
||||
|
||||
reply, err := sendCmd(o, cmd)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
if reply.System.SetRelayState.ErrCode != 0 {
|
||||
log.Printf("Failed to set relay state, error: %d\n", reply.System.SetRelayState.ErrCode)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Outlet) GetOnOff() bool {
|
||||
cmd := cmd{}
|
||||
|
||||
cmd.System.GetSysinfo = &GetSysinfo{}
|
||||
|
||||
reply, err := sendCmd(o, cmd)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false
|
||||
}
|
||||
|
||||
if reply.System.GetSysinfo.ErrCode != 0 {
|
||||
log.Printf("Failed to set relay state, error: %d\n", reply.System.GetSysinfo.ErrCode)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return reply.System.GetSysinfo.RelayState == 1
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
package ntfy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Notify struct {
|
||||
topic string
|
||||
}
|
||||
|
||||
func (n *Notify) Presence(home bool) {
|
||||
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", n.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 New(topic string) *Notify {
|
||||
ntfy := Notify{topic}
|
||||
|
||||
// @TODO Make sure the topic is valid?
|
||||
|
||||
return &ntfy
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
package wol
|
||||
|
||||
import (
|
||||
"automation/device"
|
||||
"automation/integration/google"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type computer struct {
|
||||
macAddress string
|
||||
name device.InternalName
|
||||
url string
|
||||
}
|
||||
|
||||
func NewComputer(macAddress string, name device.InternalName, url string) *computer {
|
||||
c := &computer{macAddress, name, url}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// device.Basic
|
||||
var _ device.Basic = (*computer)(nil)
|
||||
func (c *computer) GetID() device.InternalName {
|
||||
return device.InternalName(c.name)
|
||||
}
|
||||
|
||||
// device.Activate
|
||||
var _ device.Activate = (*computer)(nil)
|
||||
func (c *computer) Activate(state bool) {
|
||||
if state {
|
||||
log.Printf("Starting %s\n", c.name)
|
||||
_, err := http.Get(c.url)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
} else {
|
||||
// This is not implemented
|
||||
}
|
||||
}
|
||||
|
||||
// google.DeviceInterface
|
||||
var _ google.DeviceInterface = (*computer)(nil)
|
||||
func (*computer) IsGoogleDevice() {}
|
||||
|
||||
// google.DeviceInterface
|
||||
func (c *computer) Sync() *google.Device {
|
||||
device := google.NewDevice(c.GetID().String(), google.TypeScene)
|
||||
device.AddSceneTrait(false)
|
||||
|
||||
device.Name = google.DeviceName{
|
||||
DefaultNames: []string{
|
||||
"Computer",
|
||||
},
|
||||
Name: strings.Title(c.GetID().Name()),
|
||||
}
|
||||
room := strings.Title(c.GetID().Room())
|
||||
if len(room) > 1 {
|
||||
device.RoomHint = room
|
||||
}
|
||||
|
||||
return device
|
||||
}
|
||||
|
||||
// google.DeviceInterface
|
||||
func (c *computer) Query() google.DeviceState {
|
||||
state := google.NewDeviceState(true)
|
||||
state.Status = google.StatusSuccess
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
// google.DeviceInterface
|
||||
func (c *computer) Execute(execution google.Execution, updateState *google.DeviceState) (string, bool) {
|
||||
errCode := ""
|
||||
|
||||
switch execution.Name {
|
||||
case google.CommandActivateScene:
|
||||
c.Activate(!execution.ActivateScene.Deactivate)
|
||||
default:
|
||||
errCode = "actionNotAvailable"
|
||||
log.Printf("Command (%s) not supported\n", execution.Name)
|
||||
}
|
||||
|
||||
return errCode, true
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
package zigbee
|
||||
|
||||
import (
|
||||
"automation/device"
|
||||
"automation/home"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
paho "github.com/eclipse/paho.mqtt.golang"
|
||||
)
|
||||
|
||||
func DevicesHandler(client paho.Client, prefix string, home *home.Home) {
|
||||
var handler paho.MessageHandler = func(client paho.Client, msg paho.Message) {
|
||||
var devices []Info
|
||||
json.Unmarshal(msg.Payload(), &devices)
|
||||
|
||||
for name, d := range device.GetDevices[Device](&home.Devices) {
|
||||
d.Delete(client)
|
||||
// Delete all zigbee devices from the device list
|
||||
delete(home.Devices, name)
|
||||
}
|
||||
|
||||
for _, d := range devices {
|
||||
d.MQTTAddress = fmt.Sprintf("%s/%s", prefix, d.FriendlyName.String())
|
||||
|
||||
switch d.Description {
|
||||
case "Kettle":
|
||||
kettle := NewKettle(d, client, home.Service)
|
||||
home.AddDevice(kettle)
|
||||
case "LightSensor":
|
||||
lightSensor := NewLightSensor(d, client)
|
||||
home.AddDevice(lightSensor)
|
||||
}
|
||||
}
|
||||
|
||||
// Send sync request
|
||||
// @TODO Instead of sending a sync request we should do something like home.sync <- interface{}
|
||||
// This will then restart a timer, that way the sync will only trigger once everything has settled from multiple locations
|
||||
home.Service.RequestSync(context.Background(), home.Username)
|
||||
|
||||
// Unsubscribe again, otherwise updates here will re-add all the devices which causes issues with the light sensor
|
||||
if token := client.Unsubscribe(fmt.Sprintf("%s/bridge/devices", prefix)); token.Wait() && token.Error() != nil {
|
||||
log.Println(token.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if token := client.Subscribe(fmt.Sprintf("%s/bridge/devices", prefix), 1, handler); token.Wait() && token.Error() != nil {
|
||||
log.Println(token.Error())
|
||||
}
|
||||
}
|
|
@ -1,180 +0,0 @@
|
|||
package zigbee
|
||||
|
||||
import (
|
||||
"automation/device"
|
||||
"automation/integration/google"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
paho "github.com/eclipse/paho.mqtt.golang"
|
||||
)
|
||||
|
||||
type kettle struct {
|
||||
info Info
|
||||
|
||||
client paho.Client
|
||||
service *google.Service
|
||||
|
||||
updated chan bool
|
||||
|
||||
isOn bool
|
||||
online bool
|
||||
}
|
||||
|
||||
func NewKettle(info Info, client paho.Client, service *google.Service) *kettle {
|
||||
k := &kettle{info: info, client: client, service: service, updated: make(chan bool, 1)}
|
||||
|
||||
if token := k.client.Subscribe(k.info.MQTTAddress, 1, k.stateHandler); token.Wait() && token.Error() != nil {
|
||||
log.Println(token.Error())
|
||||
}
|
||||
|
||||
return k
|
||||
}
|
||||
|
||||
func (k *kettle) stateHandler(client paho.Client, msg paho.Message) {
|
||||
var payload OnOffState
|
||||
if err := json.Unmarshal(msg.Payload(), &payload); err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Update the internal state
|
||||
k.isOn = payload.State
|
||||
k.online = true
|
||||
|
||||
// Notify that the state has updated
|
||||
for len(k.updated) > 0 {
|
||||
<- k.updated
|
||||
}
|
||||
k.updated <- true
|
||||
|
||||
// Notify google of the updated state
|
||||
id := k.GetID().String()
|
||||
k.service.ReportState(context.Background(), id, map[string]google.DeviceState{
|
||||
id: k.getState(),
|
||||
})
|
||||
}
|
||||
|
||||
func (k *kettle) getState() google.DeviceState {
|
||||
return google.NewDeviceState(k.online).RecordOnOff(k.isOn)
|
||||
}
|
||||
|
||||
|
||||
// zigbee.Device
|
||||
var _ Device = (*kettle)(nil)
|
||||
func (k *kettle) IsZigbeeDevice() {}
|
||||
|
||||
// zigbee.Device
|
||||
func (k *kettle) Delete(client paho.Client) {
|
||||
if token := client.Unsubscribe(k.info.MQTTAddress); token.Wait() && token.Error() != nil {
|
||||
log.Println(token.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// google.DeviceInterface
|
||||
var _ google.DeviceInterface = (*kettle)(nil)
|
||||
func (*kettle) IsGoogleDevice() {}
|
||||
|
||||
// google.DeviceInterface
|
||||
func (k *kettle) Sync() *google.Device {
|
||||
device := google.NewDevice(k.GetID().String(), google.TypeKettle)
|
||||
device.AddOnOffTrait(false, false)
|
||||
|
||||
device.Name = google.DeviceName{
|
||||
DefaultNames: []string{
|
||||
"Kettle",
|
||||
},
|
||||
Name: strings.Title(k.GetID().Name()),
|
||||
}
|
||||
|
||||
device.WillReportState = true
|
||||
room := strings.Title(k.GetID().Room())
|
||||
if len(room) > 1 {
|
||||
device.RoomHint = room
|
||||
}
|
||||
|
||||
device.DeviceInfo = google.DeviceInfo{
|
||||
Manufacturer: k.info.Manufacturer,
|
||||
Model: k.info.ModelID,
|
||||
SwVersion: k.info.SoftwareBuildID,
|
||||
}
|
||||
|
||||
return device
|
||||
}
|
||||
|
||||
// google.DeviceInterface
|
||||
func (k *kettle) Query() google.DeviceState {
|
||||
state := k.getState()
|
||||
if k.online {
|
||||
state.Status = google.StatusSuccess
|
||||
} else {
|
||||
state.Status = google.StatusOffline
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
// google.DeviceInterface
|
||||
func (k *kettle) Execute(execution google.Execution, updatedState *google.DeviceState) (string, bool) {
|
||||
errCode := ""
|
||||
|
||||
switch execution.Name {
|
||||
case google.CommandOnOff:
|
||||
|
||||
// Clear the updated channel
|
||||
for len(k.updated) > 0 {
|
||||
<- k.updated
|
||||
}
|
||||
|
||||
k.SetOnOff(execution.OnOff.On)
|
||||
|
||||
// Start timeout timer
|
||||
timer := time.NewTimer(time.Second)
|
||||
|
||||
// Wait for the update or timeout
|
||||
select {
|
||||
case <- k.updated:
|
||||
updatedState.RecordOnOff(k.isOn)
|
||||
|
||||
case <- timer.C:
|
||||
// If we do not get a response in time mark the device as offline
|
||||
log.Println("Device did not respond, marking as offline")
|
||||
k.online = false
|
||||
}
|
||||
|
||||
default:
|
||||
// @TODO Should probably move the error codes to a enum
|
||||
errCode = "actionNotAvailable"
|
||||
log.Printf("Command (%s) not supported\n", execution.Name)
|
||||
}
|
||||
|
||||
return errCode, k.online
|
||||
}
|
||||
|
||||
// device.Base
|
||||
var _ device.Basic = (*kettle)(nil)
|
||||
func (k *kettle) GetID() device.InternalName {
|
||||
return k.info.FriendlyName
|
||||
}
|
||||
|
||||
// device.OnOff
|
||||
var _ device.OnOff = (*kettle)(nil)
|
||||
func (k *kettle) SetOnOff(state bool) {
|
||||
msg := "OFF"
|
||||
if state {
|
||||
msg = "ON"
|
||||
}
|
||||
|
||||
if token := k.client.Publish(fmt.Sprintf("%s/set", k.info.MQTTAddress), 1, false, fmt.Sprintf(`{ "state": "%s" }`, msg)); token.Wait() && token.Error() != nil {
|
||||
log.Println(token.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// device.OnOff
|
||||
func (k *kettle) GetOnOff() bool {
|
||||
return k.isOn
|
||||
}
|
|
@ -1,142 +0,0 @@
|
|||
package zigbee
|
||||
|
||||
import (
|
||||
"automation/device"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
paho "github.com/eclipse/paho.mqtt.golang"
|
||||
)
|
||||
|
||||
type lightSensor struct {
|
||||
info Info
|
||||
|
||||
minValue int
|
||||
maxValue int
|
||||
timeout time.Duration
|
||||
|
||||
couldBeDark bool
|
||||
isDark bool
|
||||
initialized bool
|
||||
timer *time.Timer
|
||||
}
|
||||
|
||||
type DarknessPayload struct {
|
||||
IsDark bool `json:"is_dark"`
|
||||
Updated int64 `json:"updated"`
|
||||
}
|
||||
|
||||
func NewLightSensor(info Info, client paho.Client) *lightSensor {
|
||||
l := &lightSensor{info: info}
|
||||
|
||||
// 1: 15 000 - 16 000 (Turns on to late)
|
||||
// 2: 22 000 - 30 000 (About 5-10 mins late)
|
||||
// 3: 23 000 - 30 000
|
||||
l.minValue = 23000
|
||||
l.maxValue = 25000
|
||||
|
||||
l.timeout = 5 * time.Minute
|
||||
l.timer = time.NewTimer(l.timeout)
|
||||
l.timer.Stop()
|
||||
|
||||
if token := client.Subscribe(l.info.MQTTAddress, 1, l.stateHandler); token.Wait() && token.Error() != nil {
|
||||
log.Println(token.Error())
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
<-l.timer.C
|
||||
l.isDark = l.couldBeDark
|
||||
|
||||
log.Println("Is dark:", l.isDark)
|
||||
|
||||
payload, err := json.Marshal(DarknessPayload{
|
||||
IsDark: l.isDark,
|
||||
Updated: time.Now().UnixMilli(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
if token := client.Publish(l.darknessTopic(), 1, true, payload); token.Wait() && token.Error() != nil {
|
||||
log.Println(token.Error())
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *lightSensor) darknessTopic() string {
|
||||
return fmt.Sprintf("automation/darkness/%s", l.info.FriendlyName.Room())
|
||||
}
|
||||
|
||||
func (l *lightSensor) stateHandler(client paho.Client, msg paho.Message) {
|
||||
var message LightSensorState
|
||||
if err := json.Unmarshal(msg.Payload(), &message); err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(l.isDark, l.couldBeDark, message.Illuminance, l.maxValue, l.minValue)
|
||||
|
||||
if !l.initialized {
|
||||
if message.Illuminance > l.maxValue {
|
||||
l.couldBeDark = false
|
||||
} else {
|
||||
l.couldBeDark = true
|
||||
}
|
||||
|
||||
l.initialized = true
|
||||
l.timer.Reset(time.Millisecond)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if message.Illuminance > l.maxValue {
|
||||
if l.isDark && l.couldBeDark {
|
||||
log.Println("Could be light, starting timer")
|
||||
l.couldBeDark = false
|
||||
l.timer.Reset(l.timeout)
|
||||
} else if !l.isDark {
|
||||
log.Println("Is not dark, canceling timer")
|
||||
l.couldBeDark = false
|
||||
l.timer.Stop()
|
||||
}
|
||||
} else if message.Illuminance < l.minValue {
|
||||
if !l.isDark && !l.couldBeDark {
|
||||
log.Println("Could be dark, starting timer")
|
||||
l.couldBeDark = true
|
||||
l.timer.Reset(l.timeout)
|
||||
} else if l.isDark {
|
||||
log.Println("Is dark, canceling timer")
|
||||
l.couldBeDark = true
|
||||
l.timer.Stop()
|
||||
}
|
||||
} else {
|
||||
// log.Println("In between the threshold, canceling timer for now keeping the current state")
|
||||
l.couldBeDark = l.isDark
|
||||
l.timer.Stop()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// zigbee.Device
|
||||
var _ Device = (*lightSensor)(nil)
|
||||
|
||||
func (l *lightSensor) IsZigbeeDevice() {}
|
||||
|
||||
func (l *lightSensor) Delete(client paho.Client) {
|
||||
if token := client.Unsubscribe(l.darknessTopic()); token.Wait() && token.Error() != nil {
|
||||
log.Println(token.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// device.Base
|
||||
var _ device.Basic = (*lightSensor)(nil)
|
||||
|
||||
func (l *lightSensor) GetID() device.InternalName {
|
||||
return l.info.FriendlyName
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
package zigbee
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type OnOffState struct {
|
||||
State bool
|
||||
}
|
||||
|
||||
func (k *OnOffState) UnmarshalJSON(data []byte) error {
|
||||
var payload struct {
|
||||
State string `json:"state"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k.State = payload.State == "ON"
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type RemoteAction string
|
||||
|
||||
const (
|
||||
ACTION_ON RemoteAction = "on"
|
||||
ACTION_OFF = "off"
|
||||
ACTION_BRIGHTNESS_UP = "brightness_move_up"
|
||||
ACTION_BRIGHTNESS_DOWN = "brightness_move_down"
|
||||
ACTION_BRIGHTNESS_STOP = "brightness_move_down"
|
||||
)
|
||||
|
||||
type RemoteState struct {
|
||||
Action RemoteAction `json:"action"`
|
||||
}
|
||||
|
||||
type LightSensorState struct {
|
||||
Illuminance int `json:"illuminance"`
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package zigbee
|
||||
|
||||
import (
|
||||
"automation/device"
|
||||
|
||||
paho "github.com/eclipse/paho.mqtt.golang"
|
||||
)
|
||||
|
||||
type Info struct {
|
||||
IEEEAdress string `json:"ieee_address"`
|
||||
FriendlyName device.InternalName `json:"friendly_name"`
|
||||
Description string `json:"description"`
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
ModelID string `json:"model_id"`
|
||||
SoftwareBuildID string `json:"software_build_id"`
|
||||
|
||||
MQTTAddress string `json:"-"`
|
||||
}
|
||||
|
||||
type Device interface {
|
||||
device.Basic
|
||||
|
||||
IsZigbeeDevice()
|
||||
Delete(client paho.Client)
|
||||
}
|
273
main.go
273
main.go
|
@ -1,78 +1,241 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"automation/automation"
|
||||
"automation/config"
|
||||
"automation/home"
|
||||
"automation/integration/hue"
|
||||
"automation/integration/kasa"
|
||||
"automation/integration/ntfy"
|
||||
"automation/integration/wol"
|
||||
"automation/integration/zigbee"
|
||||
"automation/presence"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
MQTT "github.com/eclipse/paho.mqtt.golang"
|
||||
"github.com/joho/godotenv"
|
||||
|
||||
paho "github.com/eclipse/paho.mqtt.golang"
|
||||
"github.com/r3labs/sse/v2"
|
||||
)
|
||||
|
||||
// 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())
|
||||
}
|
||||
|
||||
type DeviceStatus struct {
|
||||
Name string
|
||||
Present bool
|
||||
}
|
||||
|
||||
// Handler got automation/presence/+
|
||||
func presenceHandler(status chan DeviceStatus) func(MQTT.Client, MQTT.Message) {
|
||||
return func(client MQTT.Client, msg MQTT.Message) {
|
||||
device := strings.Split(msg.Topic(), "/")[2]
|
||||
value, err := strconv.Atoi(string(msg.Payload()))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println("presenceHandler", device, value)
|
||||
|
||||
status <- DeviceStatus{Name: device, Present: value == 1}
|
||||
}
|
||||
}
|
||||
|
||||
type Hue struct {
|
||||
ip string
|
||||
login string
|
||||
}
|
||||
|
||||
func (hue *Hue) listenForEvents(events chan *sse.Event) {
|
||||
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(events)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
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 lastEvent struct {
|
||||
LastEvent string `json:"last_event"`
|
||||
}
|
||||
|
||||
type owner struct {
|
||||
Rid string `json:"rid"`
|
||||
Rtype string `json:"rtype"`
|
||||
}
|
||||
|
||||
type Button struct {
|
||||
Button lastEvent `json:"button"`
|
||||
Id string `json:"id"`
|
||||
IdV1 string `json:"id_v1"`
|
||||
Owner owner `json:"owner"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type on struct {
|
||||
On bool `json:"on"`
|
||||
}
|
||||
|
||||
type typeInfo struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
CreationTime time.Time `json:"creationtime"`
|
||||
Data []json.RawMessage `json:"data"`
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
_ = godotenv.Load()
|
||||
|
||||
cfg := config.Get()
|
||||
|
||||
notify := ntfy.New(cfg.Ntfy.Topic)
|
||||
hue := hue.New(cfg.Hue.IP, cfg.Hue.Token)
|
||||
|
||||
home := home.New(cfg.Google.Username, cfg.Google.Credentials, cfg.Google.OAuthUrl)
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/assistant", home.Service.FullfillmentHandler)
|
||||
|
||||
for name, info := range cfg.Computer {
|
||||
home.AddDevice(wol.NewComputer(info.MACAddress, name, info.Url))
|
||||
host, ok := os.LookupEnv("MQTT_HOST")
|
||||
if !ok {
|
||||
host = "localhost"
|
||||
}
|
||||
|
||||
for name, ip := range cfg.Kasa.Outlets {
|
||||
home.AddDevice(kasa.NewOutlet(name, ip))
|
||||
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"
|
||||
}
|
||||
login, _ := os.LookupEnv("HUE_BRIDGE")
|
||||
|
||||
opts := paho.NewClientOptions().AddBroker(fmt.Sprintf("%s:%d", cfg.MQTT.Host, cfg.MQTT.Port))
|
||||
opts.SetClientID(cfg.MQTT.ClientID)
|
||||
opts.SetUsername(cfg.MQTT.Username)
|
||||
opts.SetPassword(cfg.MQTT.Password)
|
||||
opts.SetOrderMatters(false)
|
||||
halt := make(chan os.Signal, 1)
|
||||
signal.Notify(halt, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
client := paho.NewClient(opts)
|
||||
if token := client.Connect(); token.Wait() && token.Error() != nil {
|
||||
// @TODO Discover the bridge here
|
||||
hue := Hue{ip: "10.0.0.146", login: login}
|
||||
events := make(chan *sse.Event)
|
||||
hue.listenForEvents(events)
|
||||
|
||||
opts := MQTT.NewClientOptions().AddBroker(fmt.Sprintf("%s:%s", host, port))
|
||||
opts.SetClientID(clientID)
|
||||
opts.SetDefaultPublishHandler(defaultHandler)
|
||||
opts.SetUsername(user)
|
||||
opts.SetPassword(pass)
|
||||
|
||||
c := MQTT.NewClient(opts)
|
||||
if token := c.Connect(); token.Wait() && token.Error() != nil {
|
||||
panic(token.Error())
|
||||
}
|
||||
defer client.Disconnect(250)
|
||||
|
||||
zigbee.DevicesHandler(client, cfg.Zigbee.MQTTPrefix, home)
|
||||
|
||||
p := presence.New(client, hue, notify, home)
|
||||
defer p.Delete(client)
|
||||
|
||||
opts.SetClientID(fmt.Sprintf("%s-2", cfg.MQTT.ClientID))
|
||||
automationClient := paho.NewClient(opts)
|
||||
if token := automationClient.Connect(); token.Wait() && token.Error() != nil {
|
||||
panic(token.Error())
|
||||
}
|
||||
defer automationClient.Disconnect(250)
|
||||
|
||||
automation.RegisterAutomations(automationClient, cfg.Zigbee.MQTTPrefix, hue, notify, home, p)
|
||||
|
||||
addr := ":8090"
|
||||
srv := http.Server{
|
||||
Addr: addr,
|
||||
Handler: r,
|
||||
status := make(chan DeviceStatus, 1)
|
||||
if token := c.Subscribe("automation/presence/+", 0, presenceHandler(status)); token.Wait() && token.Error() != nil {
|
||||
fmt.Println(token.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
log.Printf("Starting server on %s (PID: %d)\n", addr, os.Getpid())
|
||||
srv.ListenAndServe()
|
||||
devices := make(map[string]bool)
|
||||
isHome := false
|
||||
|
||||
fmt.Println("Starting event loop")
|
||||
|
||||
// Event loop
|
||||
events:
|
||||
for {
|
||||
select {
|
||||
case state := <-status:
|
||||
// Update the device state
|
||||
devices[state.Name] = state.Present
|
||||
|
||||
// Check if there is any device home
|
||||
temp := false
|
||||
for key, value := range devices {
|
||||
fmt.Println(key, value)
|
||||
if value {
|
||||
temp = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Only do stuff if the state changes
|
||||
if temp == isHome {
|
||||
break
|
||||
}
|
||||
isHome = temp
|
||||
|
||||
if isHome {
|
||||
fmt.Println("Coming home")
|
||||
hue.updateFlag(41, true)
|
||||
} else {
|
||||
fmt.Println("Leaving home")
|
||||
hue.updateFlag(41, false)
|
||||
}
|
||||
|
||||
fmt.Println("Done")
|
||||
|
||||
case message := <-events:
|
||||
events := []Event{}
|
||||
json.Unmarshal(message.Data, &events)
|
||||
|
||||
for _, event := range events {
|
||||
if event.Type == "update" {
|
||||
for _, data := range event.Data {
|
||||
var typeInfo typeInfo
|
||||
json.Unmarshal(data, &typeInfo)
|
||||
|
||||
switch typeInfo.Type {
|
||||
case "button":
|
||||
fmt.Println("Button")
|
||||
var button Button
|
||||
json.Unmarshal(data, &button)
|
||||
fmt.Println(button)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case <-halt:
|
||||
break events
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
if token := c.Unsubscribe("automation/presence/+"); token.Wait() && token.Error() != nil {
|
||||
fmt.Println(token.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
c.Disconnect(250)
|
||||
}
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
package presence
|
||||
|
||||
import (
|
||||
"automation/home"
|
||||
"automation/integration/hue"
|
||||
"automation/integration/ntfy"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
paho "github.com/eclipse/paho.mqtt.golang"
|
||||
"github.com/kr/pretty"
|
||||
)
|
||||
|
||||
type Presence struct {
|
||||
devices map[string]bool
|
||||
presence bool
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
State bool `json:"state"`
|
||||
Updated int64 `json:"updated"`
|
||||
}
|
||||
|
||||
func (p *Presence) Current() bool {
|
||||
return p.presence
|
||||
}
|
||||
|
||||
func (p *Presence) devicePresenceHandler(client paho.Client, msg paho.Message) {
|
||||
name := strings.Split(msg.Topic(), "/")[2]
|
||||
|
||||
if len(msg.Payload()) == 0 {
|
||||
delete(p.devices, name)
|
||||
} else {
|
||||
var message Message
|
||||
err := json.Unmarshal(msg.Payload(), &message)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
p.devices[name] = message.State
|
||||
}
|
||||
|
||||
present := false
|
||||
pretty.Logf("Presence updated: %v\n", p.devices)
|
||||
for _, value := range p.devices {
|
||||
if value {
|
||||
present = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if p.presence != present {
|
||||
p.presence = present
|
||||
|
||||
payload, err := json.Marshal(Message{
|
||||
State: present,
|
||||
Updated: time.Now().UnixMilli(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
token := client.Publish("automation/presence", 1, true, payload)
|
||||
if token.Wait() && token.Error() != nil {
|
||||
log.Println(token.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func New(client paho.Client, hue *hue.Hue, ntfy *ntfy.Notify, home *home.Home) *Presence {
|
||||
p := &Presence{devices: make(map[string]bool), presence: false}
|
||||
|
||||
if token := client.Subscribe("automation/presence/+", 1, p.devicePresenceHandler); token.Wait() && token.Error() != nil {
|
||||
log.Println(token.Error())
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Presence) Delete(client paho.Client) {
|
||||
if token := client.Unsubscribe("automation/presence/+"); token.Wait() && token.Error() != nil {
|
||||
log.Println(token.Error())
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user