This commit is contained in:
parent
01b2d492ba
commit
c091ad0782
|
@ -13,7 +13,10 @@ kasa:
|
||||||
living_room/speakers: 10.0.0.182
|
living_room/speakers: 10.0.0.182
|
||||||
|
|
||||||
computers:
|
computers:
|
||||||
zeus:
|
living_room/zeus:
|
||||||
mac: 30:9c:23:60:9c:13
|
mac: 30:9c:23:60:9c:13
|
||||||
room: Living Room
|
room: Living Room
|
||||||
url: http://10.0.0.2:9000/start-pc?mac=30:9c:23:60:9c:13
|
url: http://10.0.0.2:9000/start-pc?mac=30:9c:23:60:9c:13
|
||||||
|
|
||||||
|
google:
|
||||||
|
username: Dreaded_X
|
||||||
|
|
78
config/config.go
Normal file
78
config/config.go
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
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"`
|
||||||
|
|
||||||
|
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"`
|
||||||
|
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
|
||||||
|
}
|
43
device/device.go
Normal file
43
device/device.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package device
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Basic interface {
|
||||||
|
GetID() InternalName
|
||||||
|
}
|
||||||
|
|
||||||
|
type OnOff interface {
|
||||||
|
SetOnOff(state bool)
|
||||||
|
GetOnOff() 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
|
||||||
|
}
|
||||||
|
|
33
device/internal_name.go
Normal file
33
device/internal_name.go
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
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, "_", " ")
|
||||||
|
room = strings.Title(room)
|
||||||
|
|
||||||
|
return room
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n InternalName) Name() string {
|
||||||
|
s := strings.Split(string(n), "/")
|
||||||
|
name := s[0]
|
||||||
|
if len(s) > 1 {
|
||||||
|
name = s[1]
|
||||||
|
}
|
||||||
|
name = strings.Title(name)
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n InternalName) String() string {
|
||||||
|
return string(n)
|
||||||
|
}
|
||||||
|
|
|
@ -1,227 +0,0 @@
|
||||||
package device
|
|
||||||
|
|
||||||
import (
|
|
||||||
"automation/integration/google"
|
|
||||||
"automation/integration/kasa"
|
|
||||||
"context"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/kr/pretty"
|
|
||||||
"google.golang.org/api/homegraph/v1"
|
|
||||||
"google.golang.org/api/option"
|
|
||||||
|
|
||||||
paho "github.com/eclipse/paho.mqtt.golang"
|
|
||||||
)
|
|
||||||
|
|
||||||
type BaseDevice interface {
|
|
||||||
GetName() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Devices struct {
|
|
||||||
Devices map[string]interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDevices() *Devices {
|
|
||||||
return &Devices{Devices: make(map[string]interface{})}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Devices) GetGoogleDevices() map[string]google.DeviceInterface {
|
|
||||||
devices := make(map[string]google.DeviceInterface)
|
|
||||||
|
|
||||||
for _, device := range d.Devices {
|
|
||||||
if gd, ok := device.(google.DeviceInterface); ok {
|
|
||||||
// Instead of using name we use the internal ID for google, that way devices can freely be renamed without causing issues with google home
|
|
||||||
devices[gd.GetID()] = gd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return devices
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Devices) GetGoogleDevice(name string) (google.DeviceInterface, error) {
|
|
||||||
device, ok := d.GetGoogleDevices()[name]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("Device does not exist")
|
|
||||||
}
|
|
||||||
|
|
||||||
return device, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Devices) GetZigbeeDevices() map[string]ZigbeeDevice {
|
|
||||||
devices := make(map[string]ZigbeeDevice)
|
|
||||||
|
|
||||||
for name, device := range d.Devices {
|
|
||||||
if zd, ok := device.(ZigbeeDevice); ok {
|
|
||||||
devices[name] = zd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return devices
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Devices) GetKasaDevices() map[string]*kasa.Kasa {
|
|
||||||
devices := make(map[string]*kasa.Kasa)
|
|
||||||
|
|
||||||
for _, device := range d.Devices {
|
|
||||||
if gd, ok := device.(*kasa.Kasa); ok {
|
|
||||||
// Instead of using name we use the internal ID for google, that way devices can freely be renamed without causing issues with google home
|
|
||||||
devices[gd.GetName()] = gd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return devices
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Devices) GetKasaDevice(name string) (*kasa.Kasa, error) {
|
|
||||||
device, ok := d.GetKasaDevices()[name]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("Device does not exist")
|
|
||||||
}
|
|
||||||
|
|
||||||
return device, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type DeviceInfo struct {
|
|
||||||
IEEEAdress string `json:"ieee_address"`
|
|
||||||
FriendlyName string `json:"friendly_name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Manufacturer string `json:"manufacturer"`
|
|
||||||
ModelID string `json:"model_id"`
|
|
||||||
SoftwareBuildID string `json:"software_build_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ZigbeeDevice interface {
|
|
||||||
GetDeviceInfo() DeviceInfo
|
|
||||||
SetState(state bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
type DeviceInterface interface {
|
|
||||||
google.DeviceInterface
|
|
||||||
SetState(state bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Provider struct {
|
|
||||||
Service *google.Service
|
|
||||||
userID string
|
|
||||||
|
|
||||||
Devices *Devices
|
|
||||||
}
|
|
||||||
|
|
||||||
type credentials []byte
|
|
||||||
type Config struct {
|
|
||||||
Credentials credentials `yaml:"credentials" envconfig:"GOOGLE_CREDENTIALS"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *credentials) Decode(value string) error {
|
|
||||||
b, err := base64.StdEncoding.DecodeString(value)
|
|
||||||
*c = b
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto populate and update the device list
|
|
||||||
func (p *Provider) devicesHandler(client paho.Client, msg paho.Message) {
|
|
||||||
var devices []DeviceInfo
|
|
||||||
json.Unmarshal(msg.Payload(), &devices)
|
|
||||||
|
|
||||||
log.Println("zigbee2mqtt devices:")
|
|
||||||
pretty.Logln(devices)
|
|
||||||
|
|
||||||
for name := range p.Devices.GetZigbeeDevices() {
|
|
||||||
// Delete all zigbee devices from the device list
|
|
||||||
delete(p.Devices.Devices, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, device := range devices {
|
|
||||||
switch device.Description {
|
|
||||||
case "Kettle":
|
|
||||||
kettle := NewKettle(device, client, p.Service)
|
|
||||||
p.Devices.Devices[kettle.GetDeviceInfo().FriendlyName] = kettle
|
|
||||||
log.Printf("Added Kettle (%s) %s\n", kettle.GetDeviceInfo().IEEEAdress, kettle.GetDeviceInfo().FriendlyName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send sync request
|
|
||||||
p.Service.RequestSync(context.Background(), p.userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewProvider(config Config, client paho.Client) *Provider {
|
|
||||||
provider := &Provider{userID: "Dreaded_X", Devices: NewDevices()}
|
|
||||||
|
|
||||||
homegraphService, err := homegraph.NewService(context.Background(), option.WithCredentialsJSON(config.Credentials))
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
provider.Service = google.NewService(provider, homegraphService)
|
|
||||||
|
|
||||||
if token := client.Subscribe("zigbee2mqtt/bridge/devices", 1, provider.devicesHandler); token.Wait() && token.Error() != nil {
|
|
||||||
log.Println(token.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return provider
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) AddDevice(device BaseDevice) {
|
|
||||||
p.Devices.Devices[device.GetName()] = device
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) Sync(_ context.Context, _ string) ([]*google.Device, error) {
|
|
||||||
var devices []*google.Device
|
|
||||||
|
|
||||||
for _, device := range p.Devices.GetGoogleDevices() {
|
|
||||||
devices = append(devices, device.Sync())
|
|
||||||
}
|
|
||||||
|
|
||||||
return devices, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) 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 := p.Devices.GetGoogleDevice(handle.ID); err == nil {
|
|
||||||
states[handle.ID] = device.Query()
|
|
||||||
} else {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return states, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) 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 := p.Devices.GetGoogleDevice(handle.ID); err == nil {
|
|
||||||
errCode, online := device.Execute(execution, &resp.UpdatedState)
|
|
||||||
|
|
||||||
// Update the state
|
|
||||||
p.Devices.Devices[handle.ID] = 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
|
|
||||||
}
|
|
98
home/home.go
Normal file
98
home/home.go
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
package home
|
||||||
|
|
||||||
|
import (
|
||||||
|
"automation/config"
|
||||||
|
"automation/device"
|
||||||
|
"automation/integration/google"
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"google.golang.org/api/homegraph/v1"
|
||||||
|
"google.golang.org/api/option"
|
||||||
|
|
||||||
|
paho "github.com/eclipse/paho.mqtt.golang"
|
||||||
|
)
|
||||||
|
|
||||||
|
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, client paho.Client) *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)
|
||||||
|
|
||||||
|
return home
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Home) AddDevice(d device.Basic) {
|
||||||
|
h.Devices[d.GetID()] = d
|
||||||
|
|
||||||
|
log.Printf("Added %s in %s (%s)\n", d.GetID().Name(), d.GetID().Room(), d.GetID())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Home) Sync(_ context.Context, _ string) ([]*google.Device, error) {
|
||||||
|
var devices []*google.Device
|
||||||
|
|
||||||
|
for _, device := range device.GetDevices[google.DeviceInterface](&h.Devices) {
|
||||||
|
devices = append(devices, device.Sync())
|
||||||
|
}
|
||||||
|
|
||||||
|
return devices, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Home) Query(_ context.Context, _ string, handles []google.DeviceHandle) (map[string]google.DeviceState, error) {
|
||||||
|
states := make(map[string]google.DeviceState)
|
||||||
|
|
||||||
|
for _, handle := range handles {
|
||||||
|
if device, err := device.GetDevice[google.DeviceInterface](&h.Devices, device.InternalName(handle.ID)); err == nil {
|
||||||
|
states[device.GetID().String()] = device.Query()
|
||||||
|
} else {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return states, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Home) Execute(_ context.Context, _ string, commands []google.Command) (*google.ExecuteResponse, error) {
|
||||||
|
resp := &google.ExecuteResponse{
|
||||||
|
UpdatedState: google.NewDeviceState(true),
|
||||||
|
FailedDevices: make(map[string]struct{ Devices []string }),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, command := range commands {
|
||||||
|
for _, execution := range command.Execution {
|
||||||
|
for _, handle := range command.Devices {
|
||||||
|
if device, err := device.GetDevice[google.DeviceInterface](&h.Devices, device.InternalName(handle.ID)); err == nil {
|
||||||
|
errCode, online := device.Execute(execution, &resp.UpdatedState)
|
||||||
|
|
||||||
|
// Update the state
|
||||||
|
h.Devices[device.GetID()] = device
|
||||||
|
if !online {
|
||||||
|
resp.OfflineDevices = append(resp.OfflineDevices, handle.ID)
|
||||||
|
} else if len(errCode) == 0 {
|
||||||
|
resp.UpdatedDevices = append(resp.UpdatedDevices, handle.ID)
|
||||||
|
} else {
|
||||||
|
e := resp.FailedDevices[errCode]
|
||||||
|
e.Devices = append(e.Devices, handle.ID)
|
||||||
|
resp.FailedDevices[errCode] = e
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package google
|
package google
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"automation/device"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
@ -10,10 +11,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type DeviceInterface interface {
|
type DeviceInterface interface {
|
||||||
|
device.Basic
|
||||||
|
|
||||||
Sync() *Device
|
Sync() *Device
|
||||||
Query() DeviceState
|
Query() DeviceState
|
||||||
Execute(execution Execution, updatedState *DeviceState) (errCode string, online bool)
|
Execute(execution Execution, updatedState *DeviceState) (errCode string, online bool)
|
||||||
GetID() string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://developers.google.com/assistant/smarthome/reference/intent/sync
|
// https://developers.google.com/assistant/smarthome/reference/intent/sync
|
||||||
|
|
|
@ -5,13 +5,16 @@ import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net"
|
"net"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This implementation is based on:
|
// This implementation is based on:
|
||||||
// https://www.softscheck.com/en/blog/tp-link-reverse-engineering/
|
// https://www.softscheck.com/en/blog/tp-link-reverse-engineering/
|
||||||
|
|
||||||
|
type Device interface {
|
||||||
|
GetIP() string
|
||||||
|
}
|
||||||
|
|
||||||
func encrypt(data []byte) []byte {
|
func encrypt(data []byte) []byte {
|
||||||
var key byte = 171
|
var key byte = 171
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
|
@ -45,18 +48,8 @@ func decrypt(data []byte) ([]byte, error) {
|
||||||
return buf, nil
|
return buf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sendCmd(kasa Device, cmd cmd) (reply, error) {
|
||||||
type Kasa struct {
|
con, err := net.Dial("tcp", fmt.Sprintf("%s:9999", kasa.GetIP()))
|
||||||
name string
|
|
||||||
ip string
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(name string, ip string) *Kasa {
|
|
||||||
return &Kasa{name, ip}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (kasa *Kasa) sendCmd(cmd cmd) (reply, error) {
|
|
||||||
con, err := net.Dial("tcp", fmt.Sprintf("%s:9999", kasa.ip))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return reply{}, err
|
return reply{}, err
|
||||||
}
|
}
|
||||||
|
@ -93,45 +86,3 @@ func (kasa *Kasa) sendCmd(cmd cmd) (reply, error) {
|
||||||
return reply, err
|
return reply, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (kasa *Kasa) SetState(on bool) {
|
|
||||||
var cmd cmd
|
|
||||||
cmd.System.SetRelayState = &SetRelayState{State: 0}
|
|
||||||
if on {
|
|
||||||
cmd.System.SetRelayState.State = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
reply, err := kasa.sendCmd(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 (kasa *Kasa) GetState() bool {
|
|
||||||
cmd := cmd{}
|
|
||||||
|
|
||||||
cmd.System.GetSysinfo = &GetSysinfo{}
|
|
||||||
|
|
||||||
reply, err := kasa.sendCmd(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
|
|
||||||
}
|
|
||||||
|
|
||||||
func (kasa *Kasa) GetName() string {
|
|
||||||
return kasa.name
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
68
integration/kasa/outlet.go
Normal file
68
integration/kasa/outlet.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
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 (o *Outlet) GetIP() string {
|
||||||
|
return o.ip
|
||||||
|
}
|
||||||
|
|
||||||
|
// device.Basic
|
||||||
|
var _ device.Basic = (*Outlet)(nil)
|
||||||
|
func (o *Outlet) GetID() device.InternalName {
|
||||||
|
return o.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// device.OnOff
|
||||||
|
var _ device.OnOff = (*Outlet)(nil)
|
||||||
|
func (o *Outlet) SetOnOff(on bool) {
|
||||||
|
var cmd cmd
|
||||||
|
cmd.System.SetRelayState = &SetRelayState{State: 0}
|
||||||
|
if on {
|
||||||
|
cmd.System.SetRelayState.State = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
reply, err := sendCmd(o, cmd)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if reply.System.SetRelayState.ErrCode != 0 {
|
||||||
|
log.Printf("Failed to set relay state, error: %d\n", reply.System.SetRelayState.ErrCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Outlet) GetOnOff() bool {
|
||||||
|
cmd := cmd{}
|
||||||
|
|
||||||
|
cmd.System.GetSysinfo = &GetSysinfo{}
|
||||||
|
|
||||||
|
reply, err := sendCmd(o, cmd)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if reply.System.GetSysinfo.ErrCode != 0 {
|
||||||
|
log.Printf("Failed to set relay state, error: %d\n", reply.System.GetSysinfo.ErrCode)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply.System.GetSysinfo.RelayState == 1
|
||||||
|
}
|
|
@ -1,37 +0,0 @@
|
||||||
package mqtt
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
paho "github.com/eclipse/paho.mqtt.golang"
|
|
||||||
)
|
|
||||||
|
|
||||||
// This is the default message handler, it just prints out the topic and message
|
|
||||||
var defaultHandler paho.MessageHandler = func(client paho.Client, msg paho.Message) {
|
|
||||||
fmt.Printf("TOPIC: %s\n", msg.Topic())
|
|
||||||
fmt.Printf("MSG: %s\n", msg.Payload())
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(host string, port int, clientID string, username string, password string) paho.Client {
|
|
||||||
opts := paho.NewClientOptions().AddBroker(fmt.Sprintf("%s:%d", host, port))
|
|
||||||
opts.SetClientID(clientID)
|
|
||||||
opts.SetDefaultPublishHandler(defaultHandler)
|
|
||||||
opts.SetUsername(username)
|
|
||||||
opts.SetPassword(password)
|
|
||||||
opts.SetOrderMatters(false)
|
|
||||||
|
|
||||||
client := paho.NewClient(opts)
|
|
||||||
if token := client.Connect(); token.Wait() && token.Error() != nil {
|
|
||||||
panic(token.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
func Delete(m paho.Client) {
|
|
||||||
if token := m.Unsubscribe("automation/presence/+"); token.Wait() && token.Error() != nil {
|
|
||||||
fmt.Println(token.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
m.Disconnect(250)
|
|
||||||
}
|
|
|
@ -11,7 +11,6 @@ type Notify struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Notify) Presence(home bool) {
|
func (n *Notify) Presence(home bool) {
|
||||||
// @TODO Maybe add list the devices that are home currently?
|
|
||||||
var description string
|
var description string
|
||||||
var actions string
|
var actions string
|
||||||
if home {
|
if home {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package device
|
package wol
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"automation/device"
|
||||||
"automation/integration/google"
|
"automation/integration/google"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -8,32 +9,48 @@ import (
|
||||||
|
|
||||||
type computer struct {
|
type computer struct {
|
||||||
macAddress string
|
macAddress string
|
||||||
name string
|
name device.InternalName
|
||||||
room string
|
|
||||||
url string
|
url string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewComputer(macAddress string, name string, room string, url string) *computer {
|
func NewComputer(macAddress string, name device.InternalName, url string) *computer {
|
||||||
c := &computer{macAddress: macAddress, name: name, room: room}
|
c := &computer{macAddress: macAddress, name: name}
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *computer) Activate(state bool) {
|
||||||
|
if state {
|
||||||
|
http.Get(c.url)
|
||||||
|
} else {
|
||||||
|
// Scene does not implement this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// device.Basic
|
||||||
|
var _ device.Basic = (*computer)(nil)
|
||||||
|
func (c *computer) GetID() device.InternalName {
|
||||||
|
return device.InternalName(c.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// google.DeviceInterface
|
||||||
|
var _ google.DeviceInterface = (*computer)(nil)
|
||||||
func (c *computer) Sync() *google.Device {
|
func (c *computer) Sync() *google.Device {
|
||||||
device := google.NewDevice(c.GetID(), google.TypeScene)
|
device := google.NewDevice(c.GetID().String(), google.TypeScene)
|
||||||
device.AddSceneTrait(false)
|
device.AddSceneTrait(false)
|
||||||
|
|
||||||
device.Name = google.DeviceName{
|
device.Name = google.DeviceName{
|
||||||
DefaultNames: []string{
|
DefaultNames: []string{
|
||||||
"Computer",
|
"Computer",
|
||||||
},
|
},
|
||||||
Name: c.name,
|
Name: c.GetID().Name(),
|
||||||
}
|
}
|
||||||
device.RoomHint = c.room
|
device.RoomHint = c.GetID().Room()
|
||||||
|
|
||||||
return device
|
return device
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// google.DeviceInterface
|
||||||
func (c *computer) Query() google.DeviceState {
|
func (c *computer) Query() google.DeviceState {
|
||||||
state := google.NewDeviceState(true)
|
state := google.NewDeviceState(true)
|
||||||
state.Status = google.StatusSuccess
|
state.Status = google.StatusSuccess
|
||||||
|
@ -41,12 +58,13 @@ func (c *computer) Query() google.DeviceState {
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// google.DeviceInterface
|
||||||
func (c *computer) Execute(execution google.Execution, updateState *google.DeviceState) (string, bool) {
|
func (c *computer) Execute(execution google.Execution, updateState *google.DeviceState) (string, bool) {
|
||||||
errCode := ""
|
errCode := ""
|
||||||
|
|
||||||
switch execution.Name {
|
switch execution.Name {
|
||||||
case google.CommandActivateScene:
|
case google.CommandActivateScene:
|
||||||
c.SetState(!execution.ActivateScene.Deactivate)
|
c.Activate(!execution.ActivateScene.Deactivate)
|
||||||
default:
|
default:
|
||||||
errCode = "actionNotAvailable"
|
errCode = "actionNotAvailable"
|
||||||
log.Printf("Command (%s) not supported\n", execution.Name)
|
log.Printf("Command (%s) not supported\n", execution.Name)
|
||||||
|
@ -54,19 +72,3 @@ func (c *computer) Execute(execution google.Execution, updateState *google.Devic
|
||||||
|
|
||||||
return errCode, true
|
return errCode, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *computer) GetID() string {
|
|
||||||
return c.macAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *computer) GetName() string {
|
|
||||||
return c.name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *computer) SetState(state bool) {
|
|
||||||
if state {
|
|
||||||
http.Get(c.url)
|
|
||||||
} else {
|
|
||||||
// Scene does not implement this
|
|
||||||
}
|
|
||||||
}
|
|
39
integration/zigbee/devices.go
Normal file
39
integration/zigbee/devices.go
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package zigbee
|
||||||
|
|
||||||
|
import (
|
||||||
|
"automation/device"
|
||||||
|
"automation/home"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
paho "github.com/eclipse/paho.mqtt.golang"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DevicesHandler(client paho.Client, 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()
|
||||||
|
// Delete all zigbee devices from the device list
|
||||||
|
delete(home.Devices, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range devices {
|
||||||
|
switch d.Description {
|
||||||
|
case "Kettle":
|
||||||
|
kettle := NewKettle(d, client, home.Service)
|
||||||
|
home.AddDevice(kettle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send sync request
|
||||||
|
home.Service.RequestSync(context.Background(), home.Username)
|
||||||
|
}
|
||||||
|
|
||||||
|
if token := client.Subscribe("zigbee2mqtt/bridge/devices", 1, handler); token.Wait() && token.Error() != nil {
|
||||||
|
log.Println(token.Error())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,19 +1,23 @@
|
||||||
package device
|
package zigbee
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"automation/device"
|
||||||
"automation/integration/google"
|
"automation/integration/google"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
paho "github.com/eclipse/paho.mqtt.golang"
|
paho "github.com/eclipse/paho.mqtt.golang"
|
||||||
)
|
)
|
||||||
|
|
||||||
type kettle struct {
|
type kettle struct {
|
||||||
info DeviceInfo
|
info Info
|
||||||
|
|
||||||
client paho.Client
|
client paho.Client
|
||||||
|
service *google.Service
|
||||||
|
|
||||||
updated chan bool
|
updated chan bool
|
||||||
|
|
||||||
timerLength time.Duration
|
timerLength time.Duration
|
||||||
|
@ -24,8 +28,19 @@ type kettle struct {
|
||||||
online bool
|
online bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kettle) getState() google.DeviceState {
|
func NewKettle(info Info, client paho.Client, service *google.Service) *kettle {
|
||||||
return google.NewDeviceState(k.online).RecordOnOff(k.isOn)
|
k := &kettle{info: info, client: client, service: service, updated: make(chan bool, 1), timerLength: 5 * time.Minute, stop: make(chan interface{})}
|
||||||
|
k.timer = time.NewTimer(k.timerLength)
|
||||||
|
k.timer.Stop()
|
||||||
|
|
||||||
|
// Start function
|
||||||
|
go k.timerFunc()
|
||||||
|
|
||||||
|
if token := k.client.Subscribe(fmt.Sprintf("zigbee2mqtt/%s", k.info.FriendlyName), 1, k.stateHandler); token.Wait() && token.Error() != nil {
|
||||||
|
log.Println(token.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return k
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kettle) stateHandler(client paho.Client, msg paho.Message) {
|
func (k *kettle) stateHandler(client paho.Client, msg paho.Message) {
|
||||||
|
@ -45,11 +60,10 @@ func (k *kettle) stateHandler(client paho.Client, msg paho.Message) {
|
||||||
k.updated <- true
|
k.updated <- true
|
||||||
|
|
||||||
// Notify google of the updated state
|
// Notify google of the updated state
|
||||||
// @TODO Fix this
|
id := k.GetID().String()
|
||||||
// id := k.GetID()
|
k.service.ReportState(context.Background(), id, map[string]google.DeviceState{
|
||||||
// s.ReportState(context.Background(), id, map[string]google.DeviceState{
|
id: k.getState(),
|
||||||
// id: k.getState(),
|
})
|
||||||
// })
|
|
||||||
|
|
||||||
if k.isOn {
|
if k.isOn {
|
||||||
k.timer.Reset(k.timerLength)
|
k.timer.Reset(k.timerLength)
|
||||||
|
@ -73,51 +87,42 @@ func (k *kettle) timerFunc() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kettle) Delete() {
|
func (k *kettle) getState() google.DeviceState {
|
||||||
// The the timer function that it needs to stop
|
return google.NewDeviceState(k.online).RecordOnOff(k.isOn)
|
||||||
k.stop <- struct{}{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewKettle(info DeviceInfo, client paho.Client, s *google.Service) *kettle {
|
|
||||||
k := &kettle{info: info, client: client, updated: make(chan bool, 1), timerLength: 5 * time.Minute, stop: make(chan interface{})}
|
|
||||||
k.timer = time.NewTimer(k.timerLength)
|
|
||||||
k.timer.Stop()
|
|
||||||
|
|
||||||
// Start function
|
// zigbee.Device
|
||||||
go k.timerFunc()
|
var _ Device = (*kettle)(nil)
|
||||||
|
func (k *kettle) Delete() {
|
||||||
|
k.stop <- struct{}{}
|
||||||
|
|
||||||
if token := k.client.Subscribe(fmt.Sprintf("zigbee2mqtt/%s", k.info.FriendlyName), 1, k.stateHandler); token.Wait() && token.Error() != nil {
|
if token := k.client.Subscribe(fmt.Sprintf("zigbee2mqtt/%s", k.info.FriendlyName), 1, k.stateHandler); token.Wait() && token.Error() != nil {
|
||||||
log.Println(token.Error())
|
log.Println(token.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return k
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kettle) Sync() *google.Device {
|
func (k *kettle) IsZigbeeDevice() bool {
|
||||||
device := google.NewDevice(k.GetID(), google.TypeKettle)
|
return true
|
||||||
device.AddOnOffTrait(false, false)
|
}
|
||||||
|
|
||||||
s := strings.Split(k.info.FriendlyName, "/")
|
|
||||||
room := ""
|
// google.DeviceInterface
|
||||||
name := s[0]
|
var _ google.DeviceInterface = (*kettle)(nil)
|
||||||
if len(s) > 1 {
|
func (k *kettle) Sync() *google.Device {
|
||||||
room = s[0]
|
device := google.NewDevice(k.GetID().String(), google.TypeKettle)
|
||||||
name = s[1]
|
device.AddOnOffTrait(false, false)
|
||||||
}
|
|
||||||
room = strings.Title(room)
|
|
||||||
name = strings.Title(name)
|
|
||||||
|
|
||||||
device.Name = google.DeviceName{
|
device.Name = google.DeviceName{
|
||||||
DefaultNames: []string{
|
DefaultNames: []string{
|
||||||
"Kettle",
|
"Kettle",
|
||||||
},
|
},
|
||||||
Name: name,
|
Name: k.GetID().Name(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// @TODO Fix reporting
|
|
||||||
// device.WillReportState = true
|
|
||||||
device.WillReportState = true
|
device.WillReportState = true
|
||||||
if len(name) > 1 {
|
room := k.GetID().Room()
|
||||||
|
if len(room) > 1 {
|
||||||
device.RoomHint = room
|
device.RoomHint = room
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,10 +135,9 @@ func (k *kettle) Sync() *google.Device {
|
||||||
return device
|
return device
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// google.DeviceInterface
|
||||||
func (k *kettle) Query() google.DeviceState {
|
func (k *kettle) Query() google.DeviceState {
|
||||||
// We just report out internal representation as it should always match the actual state
|
|
||||||
state := k.getState()
|
state := k.getState()
|
||||||
// No /get needed
|
|
||||||
if k.online {
|
if k.online {
|
||||||
state.Status = google.StatusSuccess
|
state.Status = google.StatusSuccess
|
||||||
} else {
|
} else {
|
||||||
|
@ -143,6 +147,7 @@ func (k *kettle) Query() google.DeviceState {
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// google.DeviceInterface
|
||||||
func (k *kettle) Execute(execution google.Execution, updatedState *google.DeviceState) (string, bool) {
|
func (k *kettle) Execute(execution google.Execution, updatedState *google.DeviceState) (string, bool) {
|
||||||
errCode := ""
|
errCode := ""
|
||||||
|
|
||||||
|
@ -154,7 +159,7 @@ func (k *kettle) Execute(execution google.Execution, updatedState *google.Device
|
||||||
<- k.updated
|
<- k.updated
|
||||||
}
|
}
|
||||||
|
|
||||||
k.SetState(execution.OnOff.On)
|
k.SetOnOff(execution.OnOff.On)
|
||||||
|
|
||||||
// Start timeout timer
|
// Start timeout timer
|
||||||
timer := time.NewTimer(time.Second)
|
timer := time.NewTimer(time.Second)
|
||||||
|
@ -179,19 +184,15 @@ func (k *kettle) Execute(execution google.Execution, updatedState *google.Device
|
||||||
return errCode, k.online
|
return errCode, k.online
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kettle) GetID() string {
|
// device.Base
|
||||||
return k.info.IEEEAdress
|
var _ device.Basic = (*kettle)(nil)
|
||||||
}
|
func (k *kettle) GetID() device.InternalName {
|
||||||
|
|
||||||
func (k *kettle) GetName() string {
|
|
||||||
return k.info.FriendlyName
|
return k.info.FriendlyName
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kettle) GetDeviceInfo() DeviceInfo {
|
// device.OnOff
|
||||||
return k.info
|
var _ device.OnOff = (*kettle)(nil)
|
||||||
}
|
func (k *kettle) SetOnOff(state bool) {
|
||||||
|
|
||||||
func (k *kettle) SetState(state bool) {
|
|
||||||
msg := "OFF"
|
msg := "OFF"
|
||||||
if state {
|
if state {
|
||||||
msg = "ON"
|
msg = "ON"
|
||||||
|
@ -202,3 +203,7 @@ func (k *kettle) SetState(state bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// device.OnOff
|
||||||
|
func (k *kettle) GetOnOff() bool {
|
||||||
|
return k.isOn
|
||||||
|
}
|
20
integration/zigbee/zigbee.go
Normal file
20
integration/zigbee/zigbee.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package zigbee
|
||||||
|
|
||||||
|
import "automation/device"
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Device interface {
|
||||||
|
device.Basic
|
||||||
|
|
||||||
|
// This function only exists to make this interface unique
|
||||||
|
IsZigbeeDevice() bool
|
||||||
|
Delete()
|
||||||
|
}
|
169
main.go
169
main.go
|
@ -1,178 +1,69 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"automation/device"
|
"automation/automation"
|
||||||
|
"automation/config"
|
||||||
|
"automation/home"
|
||||||
"automation/integration/hue"
|
"automation/integration/hue"
|
||||||
"automation/integration/kasa"
|
"automation/integration/kasa"
|
||||||
"automation/integration/mqtt"
|
|
||||||
"automation/integration/ntfy"
|
"automation/integration/ntfy"
|
||||||
|
"automation/integration/wol"
|
||||||
|
"automation/integration/zigbee"
|
||||||
"automation/presence"
|
"automation/presence"
|
||||||
"encoding/json"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/kelseyhightower/envconfig"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
|
|
||||||
paho "github.com/eclipse/paho.mqtt.golang"
|
paho "github.com/eclipse/paho.mqtt.golang"
|
||||||
)
|
)
|
||||||
|
|
||||||
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"`
|
|
||||||
|
|
||||||
Kasa struct {
|
|
||||||
Outlets map[string]string `yaml:"outlets"`
|
|
||||||
} `yaml:"kasa"`
|
|
||||||
|
|
||||||
Computer map[string]struct {
|
|
||||||
MACAddress string `yaml:"mac"`
|
|
||||||
Room string `yaml:"room"`
|
|
||||||
Url string `yaml:"url"`
|
|
||||||
} `yaml:"computers"`
|
|
||||||
|
|
||||||
Google device.Config `yaml:"google"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetConfig() 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// // @TODO Implement this for the other devices as well
|
|
||||||
// func GetDeviceKasa(name string) (*kasa.Kasa, error) {
|
|
||||||
// deviceGeneric, ok := devices[name]
|
|
||||||
// if !ok {
|
|
||||||
// return nil, fmt.Errorf("Device does not exist")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// device, ok := deviceGeneric.(kasa.Kasa)
|
|
||||||
// if !ok {
|
|
||||||
// return nil, fmt.Errorf("Device is not a Kasa device")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return &device, nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
func SetupBindings(client paho.Client, p *device.Provider) {
|
|
||||||
var handler paho.MessageHandler = func(client paho.Client, msg paho.Message) {
|
|
||||||
mixer, err := p.Devices.GetKasaDevice("living_room/mixer")
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
speakers, err := p.Devices.GetKasaDevice("living_room/speakers")
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var message struct {
|
|
||||||
Action string `json:"action"`
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(msg.Payload(), &message)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if message.Action == "on" {
|
|
||||||
if mixer.GetState() {
|
|
||||||
mixer.SetState(false)
|
|
||||||
speakers.SetState(false)
|
|
||||||
} else {
|
|
||||||
mixer.SetState(true)
|
|
||||||
}
|
|
||||||
} else if message.Action == "brightness_move_up" {
|
|
||||||
if speakers.GetState() {
|
|
||||||
speakers.SetState(false)
|
|
||||||
} else {
|
|
||||||
speakers.SetState(true)
|
|
||||||
mixer.SetState(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if token := client.Subscribe("test/remote", 1, handler); token.Wait() && token.Error() != nil {
|
|
||||||
log.Println(token.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
_ = godotenv.Load()
|
_ = godotenv.Load()
|
||||||
|
|
||||||
config := GetConfig()
|
cfg := config.Get()
|
||||||
|
|
||||||
// Setup all the connections to other services
|
// Setup all the connections to other services
|
||||||
client := mqtt.New(config.MQTT.Host, config.MQTT.Port, config.MQTT.ClientID, config.MQTT.Username, config.MQTT.Password)
|
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())
|
||||||
|
}
|
||||||
defer client.Disconnect(250)
|
defer client.Disconnect(250)
|
||||||
notify := ntfy.New(config.NTFY.topic)
|
notify := ntfy.New(cfg.Ntfy.Topic)
|
||||||
hue := hue.New(config.Hue.IP, config.Hue.Token)
|
hue := hue.New(cfg.Hue.IP, cfg.Hue.Token)
|
||||||
|
|
||||||
// Devices that we control and expose to google home
|
// Devices that we control and expose to google home
|
||||||
provider := device.NewProvider(config.Google, client)
|
home := home.New(cfg.Google.Username, cfg.Google.Credentials, client)
|
||||||
|
|
||||||
// Setup presence system
|
// Setup presence system
|
||||||
p := presence.New(client, hue, notify, provider)
|
p := presence.New(client, hue, notify, home)
|
||||||
defer p.Delete()
|
defer p.Delete(client)
|
||||||
|
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
r.HandleFunc("/assistant", provider.Service.FullfillmentHandler)
|
r.HandleFunc("/assistant", home.Service.FullfillmentHandler)
|
||||||
|
|
||||||
// Register computers
|
// Register computers
|
||||||
for name, info := range config.Computer {
|
for name, info := range cfg.Computer {
|
||||||
provider.AddDevice(device.NewComputer(info.MACAddress, name, info.Room, info.Url))
|
home.AddDevice(wol.NewComputer(info.MACAddress, name, info.Url))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register all kasa devies
|
// Register all kasa devies
|
||||||
for name, ip := range config.Kasa.Outlets {
|
for name, ip := range cfg.Kasa.Outlets {
|
||||||
provider.AddDevice(kasa.New(name, ip))
|
home.AddDevice(kasa.NewOutlet(name, ip))
|
||||||
}
|
}
|
||||||
|
|
||||||
SetupBindings(client, provider)
|
// Setup handler that automatically registers and updates all zigbee devices
|
||||||
|
zigbee.DevicesHandler(client, home)
|
||||||
// time.Sleep(time.Second)
|
automation.RegisterAutomations(client, hue, notify, home)
|
||||||
// pretty.Println(provider.Devices)
|
|
||||||
// pretty.Println(provider.Devices.GetGoogleDevices())
|
|
||||||
// pretty.Println(provider.Devices.GetKasaDevices())
|
|
||||||
// pretty.Println(provider.Devices.GetZigbeeDevices())
|
|
||||||
|
|
||||||
addr := ":8090"
|
addr := ":8090"
|
||||||
srv := http.Server{
|
srv := http.Server{
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
package presence
|
package presence
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"automation/device"
|
"automation/home"
|
||||||
"automation/integration/hue"
|
"automation/integration/hue"
|
||||||
"automation/integration/kasa"
|
|
||||||
"automation/integration/ntfy"
|
"automation/integration/ntfy"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -16,11 +14,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Presence struct {
|
type Presence struct {
|
||||||
client paho.Client
|
|
||||||
hue *hue.Hue
|
|
||||||
ntfy *ntfy.Notify
|
|
||||||
provider *device.Provider
|
|
||||||
|
|
||||||
devices map[string]bool
|
devices map[string]bool
|
||||||
presence bool
|
presence bool
|
||||||
}
|
}
|
||||||
|
@ -47,7 +40,7 @@ func (p *Presence) devicePresenceHandler(client paho.Client, msg paho.Message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
present := false
|
present := false
|
||||||
pretty.Println(p.devices)
|
pretty.Logf("Presence updated: %v\n", p.devices)
|
||||||
for _, value := range p.devices {
|
for _, value := range p.devices {
|
||||||
if value {
|
if value {
|
||||||
present = true
|
present = true
|
||||||
|
@ -55,7 +48,7 @@ func (p *Presence) devicePresenceHandler(client paho.Client, msg paho.Message) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println(present)
|
log.Printf("Setting overall presence: %t\n", present)
|
||||||
|
|
||||||
if p.presence != present {
|
if p.presence != present {
|
||||||
p.presence = present
|
p.presence = present
|
||||||
|
@ -76,66 +69,18 @@ func (p *Presence) devicePresenceHandler(client paho.Client, msg paho.Message) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Presence) overallPresenceHandler(client paho.Client, msg paho.Message) {
|
func New(client paho.Client, hue *hue.Hue, ntfy *ntfy.Notify, home *home.Home) *Presence {
|
||||||
if len(msg.Payload()) == 0 {
|
p := &Presence{devices: make(map[string]bool), presence: false}
|
||||||
// In this case we clear the persistent message
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var message Message
|
|
||||||
err := json.Unmarshal(msg.Payload(), &message)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Presence: %t\n", message.State)
|
if token := client.Subscribe("automation/presence/+", 1, p.devicePresenceHandler); token.Wait() && token.Error() != nil {
|
||||||
// Notify users of presence update
|
|
||||||
p.ntfy.Presence(p.presence)
|
|
||||||
|
|
||||||
// Set presence on the hue bridge
|
|
||||||
p.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 p.provider.Devices.Devices {
|
|
||||||
switch d := dev.(type) {
|
|
||||||
case *kasa.Kasa:
|
|
||||||
d.SetState(false)
|
|
||||||
case device.ZigbeeDevice:
|
|
||||||
d.SetState(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// @TODO Turn off nest thermostat
|
|
||||||
} else {
|
|
||||||
// @TODO Turn on the nest thermostat again
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(client paho.Client, hue *hue.Hue, ntfy *ntfy.Notify, provider *device.Provider) *Presence {
|
|
||||||
p := &Presence{client: client, hue: hue, ntfy: ntfy, provider: provider, devices: make(map[string]bool), presence: false}
|
|
||||||
|
|
||||||
if token := p.client.Subscribe("automation/presence", 1, p.overallPresenceHandler); token.Wait() && token.Error() != nil {
|
|
||||||
log.Println(token.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if token := p.client.Subscribe("automation/presence/+", 1, p.devicePresenceHandler); token.Wait() && token.Error() != nil {
|
|
||||||
log.Println(token.Error())
|
log.Println(token.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Presence) Delete() {
|
func (p *Presence) Delete(client paho.Client) {
|
||||||
if token := p.client.Unsubscribe("automation/presence"); token.Wait() && token.Error() != nil {
|
if token := client.Unsubscribe("automation/presence/+"); token.Wait() && token.Error() != nil {
|
||||||
log.Println(token.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if token := p.client.Unsubscribe("automation/presence/+"); token.Wait() && token.Error() != nil {
|
|
||||||
log.Println(token.Error())
|
log.Println(token.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Reference in New Issue
Block a user