From 01b2d492ba444760c6dedf41572ca839b4a18aa1 Mon Sep 17 00:00:00 2001 From: Dreaded_X Date: Fri, 18 Nov 2022 21:12:09 +0100 Subject: [PATCH] All devices are now stored in a central map, part of large refactor --- device/computer.go | 4 ++ device/kettle.go | 29 ++++++--- device/provider.go | 129 ++++++++++++++++++++++++++++++--------- integration/kasa/kasa.go | 9 ++- main.go | 112 +++++++++++++++++---------------- presence/presence.go | 27 ++++---- 6 files changed, 205 insertions(+), 105 deletions(-) diff --git a/device/computer.go b/device/computer.go index f11214e..79508f9 100644 --- a/device/computer.go +++ b/device/computer.go @@ -59,6 +59,10 @@ 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) diff --git a/device/kettle.go b/device/kettle.go index d61515c..24ded9d 100644 --- a/device/kettle.go +++ b/device/kettle.go @@ -12,7 +12,7 @@ import ( ) type kettle struct { - Info DeviceInfo + info DeviceInfo client paho.Client updated chan bool @@ -63,7 +63,7 @@ func (k *kettle) timerFunc() { select { case <- k.timer.C: 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()) } @@ -79,14 +79,14 @@ func (k *kettle) Delete() { } 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.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 { + if token := k.client.Subscribe(fmt.Sprintf("zigbee2mqtt/%s", k.info.FriendlyName), 1, k.stateHandler); token.Wait() && token.Error() != nil { log.Println(token.Error()) } @@ -97,7 +97,7 @@ func (k *kettle) Sync() *google.Device { device := google.NewDevice(k.GetID(), google.TypeKettle) device.AddOnOffTrait(false, false) - s := strings.Split(k.Info.FriendlyName, "/") + s := strings.Split(k.info.FriendlyName, "/") room := "" name := s[0] if len(s) > 1 { @@ -122,9 +122,9 @@ func (k *kettle) Sync() *google.Device { } device.DeviceInfo = google.DeviceInfo{ - Manufacturer: k.Info.Manufacturer, - Model: k.Info.ModelID, - SwVersion: k.Info.SoftwareBuildID, + Manufacturer: k.info.Manufacturer, + Model: k.info.ModelID, + SwVersion: k.info.SoftwareBuildID, } return device @@ -180,7 +180,15 @@ func (k *kettle) Execute(execution google.Execution, updatedState *google.Device } 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) { @@ -189,7 +197,8 @@ func (k *kettle) SetState(state bool) { 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()) } } + diff --git a/device/provider.go b/device/provider.go index 3009a61..753dfb1 100644 --- a/device/provider.go +++ b/device/provider.go @@ -2,9 +2,11 @@ package device import ( "automation/integration/google" + "automation/integration/kasa" "context" "encoding/base64" "encoding/json" + "fmt" "log" "github.com/kr/pretty" @@ -14,15 +16,88 @@ import ( 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"` + 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) @@ -30,10 +105,9 @@ type DeviceInterface interface { type Provider struct { Service *google.Service - userID string + userID string - devices map[string]DeviceInterface - manualDevices map[string]DeviceInterface + Devices *Devices } type credentials []byte @@ -56,15 +130,17 @@ func (p *Provider) devicesHandler(client paho.Client, msg paho.Message) { log.Println("zigbee2mqtt devices:") pretty.Logln(devices) - // Remove all automatically added devices - p.devices = p.manualDevices + 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[device.IEEEAdress] = kettle - log.Printf("Added Kettle (%s) %s\n", device.IEEEAdress, device.FriendlyName) + p.Devices.Devices[kettle.GetDeviceInfo().FriendlyName] = kettle + 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 { - 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)) if err != nil { @@ -89,15 +165,14 @@ func NewProvider(config Config, client paho.Client) *Provider { return provider } -func (p *Provider) AddDevice(device DeviceInterface) { - p.devices[device.GetID()] = device - p.manualDevices[device.GetID()] = device +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 { + for _, device := range p.Devices.GetGoogleDevices() { 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) 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() } 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) { resp := &google.ExecuteResponse{ - UpdatedState: google.NewDeviceState(true), - FailedDevices: make(map[string]struct{Devices []string}), + 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, found := p.devices[handle.ID]; found { + if device, err := p.Devices.GetGoogleDevice(handle.ID); err == nil { errCode, online := device.Execute(execution, &resp.UpdatedState) // Update the state - p.devices[handle.ID] = device + p.Devices.Devices[handle.ID] = device if !online { resp.OfflineDevices = append(resp.OfflineDevices, handle.ID) } else if len(errCode) == 0 { @@ -142,7 +217,7 @@ func (p *Provider) Execute(_ context.Context, _ string, commands []google.Comman resp.FailedDevices[errCode] = e } } 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 } - -func (p *Provider) TurnAllOff() { - for _, device := range p.devices { - device.SetState(false) - } -} diff --git a/integration/kasa/kasa.go b/integration/kasa/kasa.go index f339782..b49a588 100644 --- a/integration/kasa/kasa.go +++ b/integration/kasa/kasa.go @@ -47,11 +47,12 @@ func decrypt(data []byte) ([]byte, error) { type Kasa struct { + name string ip string } -func New(ip string) Kasa { - return Kasa{ip} +func New(name string, ip string) *Kasa { + return &Kasa{name, ip} } func (kasa *Kasa) sendCmd(cmd cmd) (reply, error) { @@ -130,3 +131,7 @@ func (kasa *Kasa) GetState() bool { return reply.System.GetSysinfo.RelayState == 1 } +func (kasa *Kasa) GetName() string { + return kasa.name +} + diff --git a/main.go b/main.go index 304020b..6addd78 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "automation/integration/mqtt" "automation/integration/ntfy" "automation/presence" + "encoding/json" "log" "net/http" "os" @@ -15,6 +16,8 @@ import ( "github.com/joho/godotenv" "github.com/kelseyhightower/envconfig" "gopkg.in/yaml.v3" + + paho "github.com/eclipse/paho.mqtt.golang" ) type config struct { @@ -73,8 +76,6 @@ func GetConfig() config { return cfg } -var devices map[string]interface{} - // // @TODO Implement this for the other devices as well // func GetDeviceKasa(name string) (*kasa.Kasa, error) { // deviceGeneric, ok := devices[name] @@ -90,79 +91,88 @@ var devices map[string]interface{} // return &device, nil // } -// func SetupBindings(m *mqtt.MQTT) { -// m.AddHandler("zigbee2mqtt/living_room/audio_remote", func(_ paho.Client, msg paho.Message) { -// mixer, err := GetDeviceKasa("living_room/mixer") -// if err != nil { -// log.Println(err) -// return -// } -// speakers, err := GetDeviceKasa("living_room/speakers") -// if err != nil { -// log.Println(err) -// return -// } +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 -// } + 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 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() { _ = godotenv.Load() config := GetConfig() - devices = make(map[string]interface{}) - // 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) defer client.Disconnect(250) notify := ntfy.New(config.NTFY.topic) 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 provider := device.NewProvider(config.Google, client) + // Setup presence system + p := presence.New(client, hue, notify, provider) + defer p.Delete() + r := mux.NewRouter() r.HandleFunc("/assistant", provider.Service.FullfillmentHandler) + // Register computers for name, info := range config.Computer { 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" srv := http.Server{ diff --git a/presence/presence.go b/presence/presence.go index 893a53a..6b2ca33 100644 --- a/presence/presence.go +++ b/presence/presence.go @@ -1,7 +1,9 @@ package presence import ( + "automation/device" "automation/integration/hue" + "automation/integration/kasa" "automation/integration/ntfy" "encoding/json" "fmt" @@ -17,6 +19,7 @@ type Presence struct { client paho.Client hue *hue.Hue ntfy *ntfy.Notify + provider *device.Provider devices map[string]bool presence bool @@ -94,18 +97,18 @@ func (p *Presence) overallPresenceHandler(client paho.Client, msg paho.Message) if !message.State { log.Println("Turn off all the devices") - // // Turn off all the devices that we manage ourselves - // provider.TurnAllOff() - // // Turn off all devices - // // @TODO Maybe allow for exceptions, could be a list in the config that we check against? - // for _, device := range devices { - // switch d := device.(type) { - // case kasa.Kasa: - // d.SetState(false) + // 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 { @@ -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 { - p := &Presence{client: client, hue: hue, ntfy: ntfy, devices: make(map[string]bool), presence: false} +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())