All devices are now stored in a central map, part of large refactor
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Dreaded_X 2022-11-18 21:12:09 +01:00
parent 6368fce40d
commit 01b2d492ba
Signed by: Dreaded_X
GPG Key ID: 76BDEC4E165D8AD9
6 changed files with 205 additions and 105 deletions

View File

@ -59,6 +59,10 @@ func (c *computer) GetID() string {
return c.macAddress return c.macAddress
} }
func (c *computer) GetName() string {
return c.name
}
func (c *computer) SetState(state bool) { func (c *computer) SetState(state bool) {
if state { if state {
http.Get(c.url) http.Get(c.url)

View File

@ -12,7 +12,7 @@ import (
) )
type kettle struct { type kettle struct {
Info DeviceInfo info DeviceInfo
client paho.Client client paho.Client
updated chan bool updated chan bool
@ -63,7 +63,7 @@ func (k *kettle) timerFunc() {
select { select {
case <- k.timer.C: case <- k.timer.C:
log.Println("Turning kettle automatically off") log.Println("Turning kettle automatically off")
if token := k.client.Publish(fmt.Sprintf("zigbee2mqtt/%s/set", k.Info.FriendlyName), 1, false, `{"state": "OFF"}`); token.Wait() && token.Error() != nil { if token := k.client.Publish(fmt.Sprintf("zigbee2mqtt/%s/set", k.info.FriendlyName), 1, false, `{"state": "OFF"}`); token.Wait() && token.Error() != nil {
log.Println(token.Error()) log.Println(token.Error())
} }
@ -79,14 +79,14 @@ func (k *kettle) Delete() {
} }
func NewKettle(info DeviceInfo, client paho.Client, s *google.Service) *kettle { 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 := &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 = time.NewTimer(k.timerLength)
k.timer.Stop() k.timer.Stop()
// Start function // Start function
go k.timerFunc() go k.timerFunc()
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())
} }
@ -97,7 +97,7 @@ func (k *kettle) Sync() *google.Device {
device := google.NewDevice(k.GetID(), google.TypeKettle) device := google.NewDevice(k.GetID(), google.TypeKettle)
device.AddOnOffTrait(false, false) device.AddOnOffTrait(false, false)
s := strings.Split(k.Info.FriendlyName, "/") s := strings.Split(k.info.FriendlyName, "/")
room := "" room := ""
name := s[0] name := s[0]
if len(s) > 1 { if len(s) > 1 {
@ -122,9 +122,9 @@ func (k *kettle) Sync() *google.Device {
} }
device.DeviceInfo = google.DeviceInfo{ device.DeviceInfo = google.DeviceInfo{
Manufacturer: k.Info.Manufacturer, Manufacturer: k.info.Manufacturer,
Model: k.Info.ModelID, Model: k.info.ModelID,
SwVersion: k.Info.SoftwareBuildID, SwVersion: k.info.SoftwareBuildID,
} }
return device return device
@ -180,7 +180,15 @@ func (k *kettle) Execute(execution google.Execution, updatedState *google.Device
} }
func (k *kettle) GetID() string { func (k *kettle) GetID() string {
return k.Info.IEEEAdress return k.info.IEEEAdress
}
func (k *kettle) GetName() string {
return k.info.FriendlyName
}
func (k *kettle) GetDeviceInfo() DeviceInfo {
return k.info
} }
func (k *kettle) SetState(state bool) { func (k *kettle) SetState(state bool) {
@ -189,7 +197,8 @@ func (k *kettle) SetState(state bool) {
msg = "ON" msg = "ON"
} }
if token := k.client.Publish(fmt.Sprintf("zigbee2mqtt/%s/set", k.Info.FriendlyName), 1, false, fmt.Sprintf(`{ "state": "%s" }`, msg)); token.Wait() && token.Error() != nil { if token := k.client.Publish(fmt.Sprintf("zigbee2mqtt/%s/set", k.info.FriendlyName), 1, false, fmt.Sprintf(`{ "state": "%s" }`, msg)); token.Wait() && token.Error() != nil {
log.Println(token.Error()) log.Println(token.Error())
} }
} }

View File

@ -2,9 +2,11 @@ package device
import ( import (
"automation/integration/google" "automation/integration/google"
"automation/integration/kasa"
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt"
"log" "log"
"github.com/kr/pretty" "github.com/kr/pretty"
@ -14,15 +16,88 @@ import (
paho "github.com/eclipse/paho.mqtt.golang" 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 { type DeviceInfo struct {
IEEEAdress string `json:"ieee_address"` IEEEAdress string `json:"ieee_address"`
FriendlyName string `json:"friendly_name"` FriendlyName string `json:"friendly_name"`
Description string `json:"description"` Description string `json:"description"`
Manufacturer string `json:"manufacturer"` Manufacturer string `json:"manufacturer"`
ModelID string `json:"model_id"` ModelID string `json:"model_id"`
SoftwareBuildID string `json:"software_build_id"` SoftwareBuildID string `json:"software_build_id"`
} }
type ZigbeeDevice interface {
GetDeviceInfo() DeviceInfo
SetState(state bool)
}
type DeviceInterface interface { type DeviceInterface interface {
google.DeviceInterface google.DeviceInterface
SetState(state bool) SetState(state bool)
@ -30,10 +105,9 @@ type DeviceInterface interface {
type Provider struct { type Provider struct {
Service *google.Service Service *google.Service
userID string userID string
devices map[string]DeviceInterface Devices *Devices
manualDevices map[string]DeviceInterface
} }
type credentials []byte type credentials []byte
@ -56,15 +130,17 @@ func (p *Provider) devicesHandler(client paho.Client, msg paho.Message) {
log.Println("zigbee2mqtt devices:") log.Println("zigbee2mqtt devices:")
pretty.Logln(devices) pretty.Logln(devices)
// Remove all automatically added devices for name := range p.Devices.GetZigbeeDevices() {
p.devices = p.manualDevices // Delete all zigbee devices from the device list
delete(p.Devices.Devices, name)
}
for _, device := range devices { for _, device := range devices {
switch device.Description { switch device.Description {
case "Kettle": case "Kettle":
kettle := NewKettle(device, client, p.Service) kettle := NewKettle(device, client, p.Service)
p.devices[device.IEEEAdress] = kettle p.Devices.Devices[kettle.GetDeviceInfo().FriendlyName] = kettle
log.Printf("Added Kettle (%s) %s\n", device.IEEEAdress, device.FriendlyName) log.Printf("Added Kettle (%s) %s\n", kettle.GetDeviceInfo().IEEEAdress, kettle.GetDeviceInfo().FriendlyName)
} }
} }
@ -73,7 +149,7 @@ func (p *Provider) devicesHandler(client paho.Client, msg paho.Message) {
} }
func NewProvider(config Config, client paho.Client) *Provider { func NewProvider(config Config, client paho.Client) *Provider {
provider := &Provider{userID: "Dreaded_X", devices: make(map[string]DeviceInterface), manualDevices: make(map[string]DeviceInterface)} provider := &Provider{userID: "Dreaded_X", Devices: NewDevices()}
homegraphService, err := homegraph.NewService(context.Background(), option.WithCredentialsJSON(config.Credentials)) homegraphService, err := homegraph.NewService(context.Background(), option.WithCredentialsJSON(config.Credentials))
if err != nil { if err != nil {
@ -89,15 +165,14 @@ func NewProvider(config Config, client paho.Client) *Provider {
return provider return provider
} }
func (p *Provider) AddDevice(device DeviceInterface) { func (p *Provider) AddDevice(device BaseDevice) {
p.devices[device.GetID()] = device p.Devices.Devices[device.GetName()] = device
p.manualDevices[device.GetID()] = device
} }
func (p *Provider) Sync(_ context.Context, _ string) ([]*google.Device, error) { func (p *Provider) Sync(_ context.Context, _ string) ([]*google.Device, error) {
var devices []*google.Device var devices []*google.Device
for _, device := range p.devices { for _, device := range p.Devices.GetGoogleDevices() {
devices = append(devices, device.Sync()) devices = append(devices, device.Sync())
} }
@ -108,10 +183,10 @@ func (p *Provider) Query(_ context.Context, _ string, handles []google.DeviceHan
states := make(map[string]google.DeviceState) states := make(map[string]google.DeviceState)
for _, handle := range handles { for _, handle := range handles {
if device, found := p.devices[handle.ID]; found { if device, err := p.Devices.GetGoogleDevice(handle.ID); err == nil {
states[handle.ID] = device.Query() states[handle.ID] = device.Query()
} else { } else {
log.Printf("Device (%s) not found\n", handle.ID) log.Println(err)
} }
} }
@ -120,18 +195,18 @@ func (p *Provider) Query(_ context.Context, _ string, handles []google.DeviceHan
func (p *Provider) Execute(_ context.Context, _ string, commands []google.Command) (*google.ExecuteResponse, error) { func (p *Provider) Execute(_ context.Context, _ string, commands []google.Command) (*google.ExecuteResponse, error) {
resp := &google.ExecuteResponse{ resp := &google.ExecuteResponse{
UpdatedState: google.NewDeviceState(true), UpdatedState: google.NewDeviceState(true),
FailedDevices: make(map[string]struct{Devices []string}), FailedDevices: make(map[string]struct{ Devices []string }),
} }
for _, command := range commands { for _, command := range commands {
for _, execution := range command.Execution { for _, execution := range command.Execution {
for _, handle := range command.Devices { for _, handle := range command.Devices {
if device, found := p.devices[handle.ID]; found { if device, err := p.Devices.GetGoogleDevice(handle.ID); err == nil {
errCode, online := device.Execute(execution, &resp.UpdatedState) errCode, online := device.Execute(execution, &resp.UpdatedState)
// Update the state // Update the state
p.devices[handle.ID] = device p.Devices.Devices[handle.ID] = device
if !online { if !online {
resp.OfflineDevices = append(resp.OfflineDevices, handle.ID) resp.OfflineDevices = append(resp.OfflineDevices, handle.ID)
} else if len(errCode) == 0 { } else if len(errCode) == 0 {
@ -142,7 +217,7 @@ func (p *Provider) Execute(_ context.Context, _ string, commands []google.Comman
resp.FailedDevices[errCode] = e resp.FailedDevices[errCode] = e
} }
} else { } else {
log.Printf("Device (%s) not found\n", handle.ID) log.Println(err)
} }
} }
} }
@ -150,9 +225,3 @@ func (p *Provider) Execute(_ context.Context, _ string, commands []google.Comman
return resp, nil return resp, nil
} }
func (p *Provider) TurnAllOff() {
for _, device := range p.devices {
device.SetState(false)
}
}

View File

@ -47,11 +47,12 @@ func decrypt(data []byte) ([]byte, error) {
type Kasa struct { type Kasa struct {
name string
ip string ip string
} }
func New(ip string) Kasa { func New(name string, ip string) *Kasa {
return Kasa{ip} return &Kasa{name, ip}
} }
func (kasa *Kasa) sendCmd(cmd cmd) (reply, error) { func (kasa *Kasa) sendCmd(cmd cmd) (reply, error) {
@ -130,3 +131,7 @@ func (kasa *Kasa) GetState() bool {
return reply.System.GetSysinfo.RelayState == 1 return reply.System.GetSysinfo.RelayState == 1
} }
func (kasa *Kasa) GetName() string {
return kasa.name
}

112
main.go
View File

@ -7,6 +7,7 @@ import (
"automation/integration/mqtt" "automation/integration/mqtt"
"automation/integration/ntfy" "automation/integration/ntfy"
"automation/presence" "automation/presence"
"encoding/json"
"log" "log"
"net/http" "net/http"
"os" "os"
@ -15,6 +16,8 @@ import (
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/kelseyhightower/envconfig" "github.com/kelseyhightower/envconfig"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
paho "github.com/eclipse/paho.mqtt.golang"
) )
type config struct { type config struct {
@ -73,8 +76,6 @@ func GetConfig() config {
return cfg return cfg
} }
var devices map[string]interface{}
// // @TODO Implement this for the other devices as well // // @TODO Implement this for the other devices as well
// func GetDeviceKasa(name string) (*kasa.Kasa, error) { // func GetDeviceKasa(name string) (*kasa.Kasa, error) {
// deviceGeneric, ok := devices[name] // deviceGeneric, ok := devices[name]
@ -90,79 +91,88 @@ var devices map[string]interface{}
// return &device, nil // return &device, nil
// } // }
// func SetupBindings(m *mqtt.MQTT) { func SetupBindings(client paho.Client, p *device.Provider) {
// m.AddHandler("zigbee2mqtt/living_room/audio_remote", func(_ paho.Client, msg paho.Message) { var handler paho.MessageHandler = func(client paho.Client, msg paho.Message) {
// mixer, err := GetDeviceKasa("living_room/mixer") mixer, err := p.Devices.GetKasaDevice("living_room/mixer")
// if err != nil { if err != nil {
// log.Println(err) log.Println(err)
// return return
// } }
// speakers, err := GetDeviceKasa("living_room/speakers") speakers, err := p.Devices.GetKasaDevice("living_room/speakers")
// if err != nil { if err != nil {
// log.Println(err) log.Println(err)
// return return
// } }
// var message struct { var message struct {
// Action string `json:"action"` Action string `json:"action"`
// } }
// err = json.Unmarshal(msg.Payload(), &message) err = json.Unmarshal(msg.Payload(), &message)
// if err != nil { if err != nil {
// log.Println(err) log.Println(err)
// return return
// } }
// if message.Action == "on" { if message.Action == "on" {
// if mixer.GetState() { if mixer.GetState() {
// mixer.SetState(false) mixer.SetState(false)
// speakers.SetState(false) speakers.SetState(false)
// } else { } else {
// mixer.SetState(true) mixer.SetState(true)
// } }
// } else if message.Action == "brightness_move_up" { } else if message.Action == "brightness_move_up" {
// if speakers.GetState() { if speakers.GetState() {
// speakers.SetState(false) speakers.SetState(false)
// } else { } else {
// speakers.SetState(true) speakers.SetState(true)
// mixer.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() config := GetConfig()
devices = make(map[string]interface{})
// 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) client := mqtt.New(config.MQTT.Host, config.MQTT.Port, config.MQTT.ClientID, config.MQTT.Username, config.MQTT.Password)
defer client.Disconnect(250) defer client.Disconnect(250)
notify := ntfy.New(config.NTFY.topic) notify := ntfy.New(config.NTFY.topic)
hue := hue.New(config.Hue.IP, config.Hue.Token) hue := hue.New(config.Hue.IP, config.Hue.Token)
// Setup presence system
p := presence.New(client, hue, notify)
defer p.Delete()
// Register all kasa devies
for name, ip := range config.Kasa.Outlets {
devices[name] = kasa.New(ip)
}
// Devices that we control and expose to google home // Devices that we control and expose to google home
provider := device.NewProvider(config.Google, client) provider := device.NewProvider(config.Google, client)
// Setup presence system
p := presence.New(client, hue, notify, provider)
defer p.Delete()
r := mux.NewRouter() r := mux.NewRouter()
r.HandleFunc("/assistant", provider.Service.FullfillmentHandler) r.HandleFunc("/assistant", provider.Service.FullfillmentHandler)
// Register computers
for name, info := range config.Computer { for name, info := range config.Computer {
provider.AddDevice(device.NewComputer(info.MACAddress, name, info.Room, info.Url)) provider.AddDevice(device.NewComputer(info.MACAddress, name, info.Room, info.Url))
} }
// Presence // Register all kasa devies
for name, ip := range config.Kasa.Outlets {
provider.AddDevice(kasa.New(name, ip))
}
SetupBindings(client, provider)
// time.Sleep(time.Second)
// 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{

View File

@ -1,7 +1,9 @@
package presence package presence
import ( import (
"automation/device"
"automation/integration/hue" "automation/integration/hue"
"automation/integration/kasa"
"automation/integration/ntfy" "automation/integration/ntfy"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -17,6 +19,7 @@ type Presence struct {
client paho.Client client paho.Client
hue *hue.Hue hue *hue.Hue
ntfy *ntfy.Notify ntfy *ntfy.Notify
provider *device.Provider
devices map[string]bool devices map[string]bool
presence bool presence bool
@ -94,18 +97,18 @@ func (p *Presence) overallPresenceHandler(client paho.Client, msg paho.Message)
if !message.State { if !message.State {
log.Println("Turn off all the devices") log.Println("Turn off all the devices")
// // Turn off all the devices that we manage ourselves
// provider.TurnAllOff()
// // Turn off all devices // Turn off all devices
// // @TODO Maybe allow for exceptions, could be a list in the config that we check against? // @TODO Maybe allow for exceptions, could be a list in the config that we check against?
// for _, device := range devices { for _, dev := range p.provider.Devices.Devices {
// switch d := device.(type) { switch d := dev.(type) {
// case kasa.Kasa: case *kasa.Kasa:
// d.SetState(false) d.SetState(false)
case device.ZigbeeDevice:
d.SetState(false)
}
// } }
// }
// @TODO Turn off nest thermostat // @TODO Turn off nest thermostat
} else { } else {
@ -113,8 +116,8 @@ func (p *Presence) overallPresenceHandler(client paho.Client, msg paho.Message)
} }
} }
func New(client paho.Client, hue *hue.Hue, ntfy *ntfy.Notify) *Presence { func New(client paho.Client, hue *hue.Hue, ntfy *ntfy.Notify, provider *device.Provider) *Presence {
p := &Presence{client: client, hue: hue, ntfy: ntfy, devices: make(map[string]bool), presence: false} 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 { if token := p.client.Subscribe("automation/presence", 1, p.overallPresenceHandler); token.Wait() && token.Error() != nil {
log.Println(token.Error()) log.Println(token.Error())