Compare commits
45 Commits
feature/ss
...
master
Author | SHA1 | Date | |
---|---|---|---|
d6bbd78bec | |||
d42afecd67 | |||
51958a0542 | |||
2b4e66b978 | |||
eef5385f75 | |||
8e0cc2140f | |||
10351018c0 | |||
00f623ab45 | |||
abda5cc24f | |||
ca976b1143 | |||
f13ee65ead | |||
780f633c90 | |||
6f5b3d13f7 | |||
501775654f | |||
4227bd92b5 | |||
301551596a | |||
5aefcf0157 | |||
20e7e830a6 | |||
c091ad0782 | |||
01b2d492ba | |||
6368fce40d | |||
2df59cdb17 | |||
c49ee841fd | |||
07fa5fc986 | |||
4c023ad933 | |||
2aff4d937a | |||
028ede0721 | |||
3dac6c66f6 | |||
32f3d013f8 | |||
9f4be2d76e | |||
644f038732 | |||
656b040cdc | |||
bfeedece77 | |||
ad5b8f9d29 | |||
dd03ae56ee | |||
dace0eba29 | |||
76a8c5e620 | |||
8b9e139b36 | |||
748e12b1e3 | |||
c8a61c0a8a | |||
0936242a41 | |||
c6bf97c3db | |||
e5a6b75f62 | |||
e3edee3e09 | |||
b4031e4198 |
|
@ -1,5 +1,5 @@
|
||||||
.git/
|
.git/
|
||||||
storage/
|
storage/
|
||||||
automation
|
app
|
||||||
.env
|
.env
|
||||||
tmp/
|
tmp/
|
||||||
|
|
25
.drone.yml
25
.drone.yml
|
@ -17,22 +17,25 @@ steps:
|
||||||
- name: socket
|
- name: socket
|
||||||
path: /var/run/docker.sock
|
path: /var/run/docker.sock
|
||||||
environment:
|
environment:
|
||||||
MQTT_HOST:
|
MQTT_PASSWORD:
|
||||||
from_secret: MQTT_HOST
|
from_secret: MQTT_PASSWORD
|
||||||
MQTT_PORT:
|
HUE_TOKEN:
|
||||||
from_secret: MQTT_PORT
|
from_secret: HUE_TOKEN
|
||||||
MQTT_USER:
|
NTFY_TOPIC:
|
||||||
from_secret: MQTT_USER
|
from_secret: NTFY_TOPIC
|
||||||
MQTT_PASS:
|
GOOGLE_OAUTH_URL:
|
||||||
from_secret: MQTT_PASS
|
from_secret: GOOGLE_OAUTH_URL
|
||||||
HUE_BRIDGE:
|
GOOGLE_CREDENTIALS:
|
||||||
from_secret: HUE_BRIDGE
|
from_secret: GOOGLE_CREDENTIALS
|
||||||
commands:
|
commands:
|
||||||
- docker stop automation || true
|
- docker stop automation || true
|
||||||
|
|
||||||
- docker rm automation || true
|
- docker rm automation || true
|
||||||
|
|
||||||
- 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
|
- 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
|
||||||
|
|
||||||
when:
|
when:
|
||||||
branch:
|
branch:
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,4 +1,4 @@
|
||||||
storage/
|
storage/
|
||||||
automation
|
app
|
||||||
.env
|
.env
|
||||||
tmp/
|
tmp/
|
||||||
|
|
|
@ -7,12 +7,13 @@ RUN go mod download
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN go build
|
RUN go build -o app
|
||||||
|
|
||||||
|
|
||||||
FROM golang:alpine
|
FROM golang:alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=build-automation /src/automation /app/automation
|
COPY --from=build-automation /src/app /app/app
|
||||||
|
COPY --from=build-automation /src/config.yml /app/config.yml
|
||||||
|
|
||||||
CMD ["/app/automation"]
|
CMD ["/app/app"]
|
||||||
|
|
46
automation/automation.go
Normal file
46
automation/automation.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
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)
|
||||||
|
}
|
14
automation/dark.go
Normal file
14
automation/dark.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
56
automation/frontdoor.go
Normal file
56
automation/frontdoor.go
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
42
automation/kettle.go
Normal file
42
automation/kettle.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
43
automation/mixer.go
Normal file
43
automation/mixer.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
42
automation/presence.go
Normal file
42
automation/presence.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
23
automation/zeus.go
Normal file
23
automation/zeus.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
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
Normal file
25
config.yml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
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
|
83
config/config.go
Normal file
83
config/config.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
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
|
||||||
|
}
|
47
device/device.go
Normal file
47
device/device.go
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
31
device/internal_name.go
Normal file
31
device/internal_name.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
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,14 +1,39 @@
|
||||||
module automation
|
module automation
|
||||||
|
|
||||||
go 1.17
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/eclipse/paho.mqtt.golang v1.3.5
|
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/joho/godotenv v1.4.0
|
||||||
github.com/kelvins/sunrisesunset v0.0.0-20210220141756-39fa1bd816d5
|
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
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
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/gorilla/websocket v1.4.2 // indirect
|
||||||
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 // 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
|
||||||
|
gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
164
go.sum
164
go.sum
|
@ -1,14 +1,172 @@
|
||||||
|
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 h1:sWtmgNxYM9P2sP+xEItMozsR3w0cqZFlqnNN1bdl41Y=
|
||||||
github.com/eclipse/paho.mqtt.golang v1.3.5/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc=
|
github.com/eclipse/paho.mqtt.golang v1.3.5/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc=
|
||||||
|
github.com/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 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/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 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/kelvins/sunrisesunset v0.0.0-20210220141756-39fa1bd816d5 h1:ouekCqYkMw4QXFCaLyYqjBe99/MUW4Qf3DJhCRh1G18=
|
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||||
github.com/kelvins/sunrisesunset v0.0.0-20210220141756-39fa1bd816d5/go.mod h1:3oZ7G+fb8Z8KF+KPHxeDO3GWpEjgvk/f+d/yaxmDRT4=
|
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/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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U=
|
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/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-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-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-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.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
Normal file
96
home/home.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
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
|
||||||
|
}
|
91
integration/google/command.go
Normal file
91
integration/google/command.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
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"`
|
||||||
|
}
|
51
integration/google/device.go
Normal file
51
integration/google/device.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
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{}),
|
||||||
|
}
|
||||||
|
}
|
246
integration/google/handler.go
Normal file
246
integration/google/handler.go
Normal file
|
@ -0,0 +1,246 @@
|
||||||
|
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"))
|
||||||
|
}
|
||||||
|
}
|
77
integration/google/intent.go
Normal file
77
integration/google/intent.go
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
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
|
||||||
|
}
|
101
integration/google/service.go
Normal file
101
integration/google/service.go
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
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
|
||||||
|
}
|
79
integration/google/state.go
Normal file
79
integration/google/state.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
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
|
||||||
|
}
|
10
integration/google/status.go
Normal file
10
integration/google/status.go
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package google
|
||||||
|
|
||||||
|
type Status string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusSuccess Status = "SUCCESS"
|
||||||
|
StatusOffline = "OFFLINE"
|
||||||
|
StatusException = "EXCEPTIONS"
|
||||||
|
StatusError = "ERROR"
|
||||||
|
)
|
72
integration/google/trait.go
Normal file
72
integration/google/trait.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
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
|
||||||
|
}
|
9
integration/google/type.go
Normal file
9
integration/google/type.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package google
|
||||||
|
|
||||||
|
type Type string
|
||||||
|
|
||||||
|
// https://developers.google.com/assistant/smarthome/guides
|
||||||
|
const (
|
||||||
|
TypeKettle = "action.devices.types.KETTLE"
|
||||||
|
TypeScene = "action.devices.types.SCENE"
|
||||||
|
)
|
60
integration/hue/events.go
Normal file
60
integration/hue/events.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package hue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EventType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Update EventType = "update"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeviceType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Light DeviceType = "light"
|
||||||
|
GroupedLight = "grouped_light"
|
||||||
|
Button = "button"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LastEvent string
|
||||||
|
|
||||||
|
const (
|
||||||
|
InitialPress LastEvent = "initial_press"
|
||||||
|
ShortPress = "short_press"
|
||||||
|
)
|
||||||
|
|
||||||
|
type device struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
IDv1 string `json:"id_v1"`
|
||||||
|
Owner struct {
|
||||||
|
Rid string `json:"rid"`
|
||||||
|
Rtype string `json:"rtype"`
|
||||||
|
} `json:"owner"`
|
||||||
|
Type DeviceType `json:"type"`
|
||||||
|
|
||||||
|
On *struct {
|
||||||
|
On bool `json:"on"`
|
||||||
|
} `json:"on"`
|
||||||
|
|
||||||
|
Dimming *struct {
|
||||||
|
Brightness float32 `json:"brightness"`
|
||||||
|
} `json:"dimming"`
|
||||||
|
|
||||||
|
ColorTemperature *struct {
|
||||||
|
Mirek int `json:"mirek"`
|
||||||
|
MirekValid bool `json:"mirek_valid"`
|
||||||
|
} `json:"color_temperature"`
|
||||||
|
|
||||||
|
Button *struct {
|
||||||
|
LastEvent LastEvent `json:"last_event"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
CreationTime time.Time `json:"creationtime"`
|
||||||
|
Data []device `json:"data"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type EventType `json:"type"`
|
||||||
|
}
|
56
integration/hue/hue.go
Normal file
56
integration/hue/hue.go
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
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
|
||||||
|
}
|
92
integration/kasa/kasa.go
Normal file
92
integration/kasa/kasa.go
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
26
integration/kasa/message.go
Normal file
26
integration/kasa/message.go
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
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"`
|
||||||
|
}
|
71
integration/kasa/outlet.go
Normal file
71
integration/kasa/outlet.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
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
|
||||||
|
}
|
43
integration/ntfy/ntfy.go
Normal file
43
integration/ntfy/ntfy.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
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
|
||||||
|
}
|
87
integration/wol/computer.go
Normal file
87
integration/wol/computer.go
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
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
|
||||||
|
}
|
52
integration/zigbee/devices.go
Normal file
52
integration/zigbee/devices.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
180
integration/zigbee/kettle.go
Normal file
180
integration/zigbee/kettle.go
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
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
|
||||||
|
}
|
142
integration/zigbee/light_sensor.go
Normal file
142
integration/zigbee/light_sensor.go
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
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
|
||||||
|
}
|
38
integration/zigbee/payload.go
Normal file
38
integration/zigbee/payload.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
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"`
|
||||||
|
}
|
25
integration/zigbee/zigbee.go
Normal file
25
integration/zigbee/zigbee.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
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)
|
||||||
|
}
|
201
main.go
201
main.go
|
@ -1,169 +1,78 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"automation/automation"
|
||||||
|
"automation/config"
|
||||||
|
"automation/home"
|
||||||
|
"automation/integration/hue"
|
||||||
|
"automation/integration/kasa"
|
||||||
|
"automation/integration/ntfy"
|
||||||
|
"automation/integration/wol"
|
||||||
|
"automation/integration/zigbee"
|
||||||
|
"automation/presence"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
MQTT "github.com/eclipse/paho.mqtt.golang"
|
"github.com/gorilla/mux"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
|
|
||||||
|
paho "github.com/eclipse/paho.mqtt.golang"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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) 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
_ = godotenv.Load()
|
_ = godotenv.Load()
|
||||||
|
|
||||||
host, ok := os.LookupEnv("MQTT_HOST")
|
cfg := config.Get()
|
||||||
if !ok {
|
|
||||||
host = "localhost"
|
|
||||||
}
|
|
||||||
port, ok := os.LookupEnv("MQTT_PORT")
|
|
||||||
if !ok {
|
|
||||||
port = "1883"
|
|
||||||
}
|
|
||||||
user, ok := os.LookupEnv("MQTT_USER")
|
|
||||||
if !ok {
|
|
||||||
user = "test"
|
|
||||||
}
|
|
||||||
pass, ok := os.LookupEnv("MQTT_PASS")
|
|
||||||
if !ok {
|
|
||||||
pass = "test"
|
|
||||||
}
|
|
||||||
clientID, ok := os.LookupEnv("MQTT_CLIENT_ID")
|
|
||||||
if !ok {
|
|
||||||
clientID = "automation"
|
|
||||||
}
|
|
||||||
login, _ := os.LookupEnv("HUE_BRIDGE")
|
|
||||||
|
|
||||||
halt := make(chan os.Signal, 1)
|
notify := ntfy.New(cfg.Ntfy.Topic)
|
||||||
signal.Notify(halt, os.Interrupt, syscall.SIGTERM)
|
hue := hue.New(cfg.Hue.IP, cfg.Hue.Token)
|
||||||
|
|
||||||
// @TODO Discover the bridge here
|
home := home.New(cfg.Google.Username, cfg.Google.Credentials, cfg.Google.OAuthUrl)
|
||||||
hue := Hue{ip: "10.0.0.146", login: login}
|
r := mux.NewRouter()
|
||||||
|
r.HandleFunc("/assistant", home.Service.FullfillmentHandler)
|
||||||
|
|
||||||
opts := MQTT.NewClientOptions().AddBroker(fmt.Sprintf("%s:%s", host, port))
|
for name, info := range cfg.Computer {
|
||||||
opts.SetClientID(clientID)
|
home.AddDevice(wol.NewComputer(info.MACAddress, name, info.Url))
|
||||||
opts.SetDefaultPublishHandler(defaultHandler)
|
}
|
||||||
opts.SetUsername(user)
|
|
||||||
opts.SetPassword(pass)
|
|
||||||
|
|
||||||
c := MQTT.NewClient(opts)
|
for name, ip := range cfg.Kasa.Outlets {
|
||||||
if token := c.Connect(); token.Wait() && token.Error() != nil {
|
home.AddDevice(kasa.NewOutlet(name, ip))
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
client := paho.NewClient(opts)
|
||||||
|
if token := client.Connect(); token.Wait() && token.Error() != nil {
|
||||||
panic(token.Error())
|
panic(token.Error())
|
||||||
}
|
}
|
||||||
|
defer client.Disconnect(250)
|
||||||
|
|
||||||
status := make(chan DeviceStatus, 1)
|
zigbee.DevicesHandler(client, cfg.Zigbee.MQTTPrefix, home)
|
||||||
if token := c.Subscribe("automation/presence/+", 0, presenceHandler(status)); token.Wait() && token.Error() != nil {
|
|
||||||
fmt.Println(token.Error())
|
p := presence.New(client, hue, notify, home)
|
||||||
os.Exit(1)
|
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
devices := make(map[string]bool)
|
log.Printf("Starting server on %s (PID: %d)\n", addr, os.Getpid())
|
||||||
isHome := false
|
srv.ListenAndServe()
|
||||||
|
|
||||||
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 <-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)
|
|
||||||
}
|
}
|
||||||
|
|
87
presence/presence.go
Normal file
87
presence/presence.go
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
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