From 5aefcf01572169df974dc9b1cfff7fd7d6f8899f Mon Sep 17 00:00:00 2001 From: Dreaded_X Date: Sat, 19 Nov 2022 04:36:40 +0100 Subject: [PATCH] More refactoring, moved kettle auto off out of the kettle implementation and into a seperate automation --- .dockerignore | 2 +- automation/automation.go | 1 + automation/kettle.go | 50 +++++++++++++++++++++++++++++++++++ automation/mixer.go | 5 ++-- home/home.go | 4 +-- integration/google/handler.go | 1 + integration/kasa/kasa.go | 4 +++ integration/kasa/outlet.go | 3 +++ integration/wol/computer.go | 3 +++ integration/zigbee/devices.go | 2 ++ integration/zigbee/kettle.go | 47 ++++++-------------------------- integration/zigbee/zigbee.go | 3 +-- main.go | 42 +++++++++++++++-------------- 13 files changed, 99 insertions(+), 68 deletions(-) create mode 100644 automation/kettle.go diff --git a/.dockerignore b/.dockerignore index afcd0b0..69eed65 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,5 @@ .git/ storage/ -automation +app .env tmp/ diff --git a/automation/automation.go b/automation/automation.go index 518c88e..9ac823b 100644 --- a/automation/automation.go +++ b/automation/automation.go @@ -11,4 +11,5 @@ import ( func RegisterAutomations(client paho.Client, hue *hue.Hue, notify *ntfy.Notify, home *home.Home) { presenceAutomation(client, hue, notify, home) mixerAutomation(client, home) + kettleAutomation(client, home) } diff --git a/automation/kettle.go b/automation/kettle.go new file mode 100644 index 0000000..cd51d2c --- /dev/null +++ b/automation/kettle.go @@ -0,0 +1,50 @@ +package automation + +import ( + "automation/device" + "automation/home" + "fmt" + "log" + "time" + + paho "github.com/eclipse/paho.mqtt.golang" +) + +func kettleAutomation(client paho.Client, home *home.Home) { + const name = "kitchen/kettle" + const length = 5 * time.Minute + + timer := time.NewTimer(length) + + var handler paho.MessageHandler = func(c paho.Client, m paho.Message) { + kettle, err := device.GetDevice[device.OnOff](&home.Devices, name) + if err != nil { + log.Println(err) + return + } + + if kettle.GetOnOff() { + timer.Reset(length) + } else { + timer.Stop() + } + } + + if token := client.Subscribe(fmt.Sprintf("zigbee2mqtt/%s", name), 1, handler); token.Wait() && token.Error() != nil { + log.Println(token.Error()) + } + + 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) + } + }() +} diff --git a/automation/mixer.go b/automation/mixer.go index 367a4c0..2ff67e7 100644 --- a/automation/mixer.go +++ b/automation/mixer.go @@ -3,7 +3,6 @@ package automation import ( "automation/device" "automation/home" - "automation/integration/kasa" "encoding/json" "log" @@ -12,12 +11,12 @@ import ( func mixerAutomation(client paho.Client, home *home.Home) { var handler paho.MessageHandler = func(client paho.Client, msg paho.Message) { - mixer, err := device.GetDevice[*kasa.Outlet](&home.Devices, "living_room/mixer") + mixer, err := device.GetDevice[device.OnOff](&home.Devices, "living_room/mixer") if err != nil { log.Println(err) return } - speakers, err := device.GetDevice[*kasa.Outlet](&home.Devices, "living_room/speakers") + speakers, err := device.GetDevice[device.OnOff](&home.Devices, "living_room/speakers") if err != nil { log.Println(err) return diff --git a/home/home.go b/home/home.go index 455fede..4ea36c5 100644 --- a/home/home.go +++ b/home/home.go @@ -9,8 +9,6 @@ import ( "google.golang.org/api/homegraph/v1" "google.golang.org/api/option" - - paho "github.com/eclipse/paho.mqtt.golang" ) type Home struct { @@ -21,7 +19,7 @@ type Home struct { } // Auto populate and update the device list -func New(username string, credentials config.Credentials, client paho.Client) *Home { +func New(username string, credentials config.Credentials) *Home { home := &Home{Username: username, Devices: make(map[device.InternalName]device.Basic)} homegraphService, err := homegraph.NewService(context.Background(), option.WithCredentialsJSON(credentials)) diff --git a/integration/google/handler.go b/integration/google/handler.go index 594b836..e28ed21 100644 --- a/integration/google/handler.go +++ b/integration/google/handler.go @@ -13,6 +13,7 @@ import ( type DeviceInterface interface { device.Basic + IsGoogleDevice() Sync() *Device Query() DeviceState Execute(execution Execution, updatedState *DeviceState) (errCode string, online bool) diff --git a/integration/kasa/kasa.go b/integration/kasa/kasa.go index 0cc5c86..fe1a25e 100644 --- a/integration/kasa/kasa.go +++ b/integration/kasa/kasa.go @@ -1,6 +1,7 @@ package kasa import ( + "automation/device" "bytes" "encoding/binary" "encoding/json" @@ -12,6 +13,9 @@ import ( // https://www.softscheck.com/en/blog/tp-link-reverse-engineering/ type Device interface { + device.Basic + + IsKasaDevice() GetIP() string } diff --git a/integration/kasa/outlet.go b/integration/kasa/outlet.go index 57e375e..0c2317a 100644 --- a/integration/kasa/outlet.go +++ b/integration/kasa/outlet.go @@ -17,6 +17,9 @@ func NewOutlet(name device.InternalName, ip string) *Outlet { // kasa.Device var _ Device = (*Outlet)(nil) +func (*Outlet) IsKasaDevice() {} + +// kasa.Device func (o *Outlet) GetIP() string { return o.ip } diff --git a/integration/wol/computer.go b/integration/wol/computer.go index f8582f7..3691e0c 100644 --- a/integration/wol/computer.go +++ b/integration/wol/computer.go @@ -35,6 +35,9 @@ func (c *computer) GetID() device.InternalName { // 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) diff --git a/integration/zigbee/devices.go b/integration/zigbee/devices.go index 1e1ebc7..e66f298 100644 --- a/integration/zigbee/devices.go +++ b/integration/zigbee/devices.go @@ -30,6 +30,8 @@ func DevicesHandler(client paho.Client, home *home.Home) { } // 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) } diff --git a/integration/zigbee/kettle.go b/integration/zigbee/kettle.go index 7b1169e..9e2adab 100644 --- a/integration/zigbee/kettle.go +++ b/integration/zigbee/kettle.go @@ -20,21 +20,12 @@ type kettle struct { updated chan bool - timerLength time.Duration - timer *time.Timer - stop chan interface{} - 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), timerLength: 5 * time.Minute, stop: make(chan interface{})} - k.timer = time.NewTimer(k.timerLength) - k.timer.Stop() - - // Start function - go k.timerFunc() + k := &kettle{info: info, client: client, service: service, updated: make(chan bool, 1)} if token := k.client.Subscribe(fmt.Sprintf("zigbee2mqtt/%s", k.info.FriendlyName), 1, k.stateHandler); token.Wait() && token.Error() != nil { log.Println(token.Error()) @@ -64,27 +55,6 @@ func (k *kettle) stateHandler(client paho.Client, msg paho.Message) { k.service.ReportState(context.Background(), id, map[string]google.DeviceState{ id: k.getState(), }) - - if k.isOn { - k.timer.Reset(k.timerLength) - } else { - k.timer.Stop() - } -} - -func (k *kettle) timerFunc() { - for { - 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 { - log.Println(token.Error()) - } - - case <- k.stop: - return - } - } } func (k *kettle) getState() google.DeviceState { @@ -94,21 +64,20 @@ func (k *kettle) getState() google.DeviceState { // zigbee.Device var _ Device = (*kettle)(nil) -func (k *kettle) Delete() { - k.stop <- struct{}{} +func (k *kettle) IsZigbeeDevice() {} - if token := k.client.Subscribe(fmt.Sprintf("zigbee2mqtt/%s", k.info.FriendlyName), 1, k.stateHandler); token.Wait() && token.Error() != nil { +// zigbee.Device +func (k *kettle) Delete() { + if token := k.client.Unsubscribe(fmt.Sprintf("zigbee2mqtt/%s", k.info.FriendlyName)); token.Wait() && token.Error() != nil { log.Println(token.Error()) } } -func (k *kettle) IsZigbeeDevice() bool { - return true -} - - // 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) diff --git a/integration/zigbee/zigbee.go b/integration/zigbee/zigbee.go index aaec6bc..21aa6ec 100644 --- a/integration/zigbee/zigbee.go +++ b/integration/zigbee/zigbee.go @@ -14,7 +14,6 @@ type Info struct { type Device interface { device.Basic - // This function only exists to make this interface unique - IsZigbeeDevice() bool + IsZigbeeDevice() Delete() } diff --git a/main.go b/main.go index 3e0f99a..b256e53 100644 --- a/main.go +++ b/main.go @@ -26,7 +26,21 @@ func main() { cfg := config.Get() - // Setup all the connections to other services + notify := ntfy.New(cfg.Ntfy.Topic) + hue := hue.New(cfg.Hue.IP, cfg.Hue.Token) + + home := home.New(cfg.Google.Username, cfg.Google.Credentials) + r := mux.NewRouter() + r.HandleFunc("/assistant", home.Service.FullfillmentHandler) + + for name, info := range cfg.Computer { + home.AddDevice(wol.NewComputer(info.MACAddress, name, info.Url)) + } + + for name, ip := range cfg.Kasa.Outlets { + 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) @@ -38,32 +52,20 @@ func main() { panic(token.Error()) } defer client.Disconnect(250) - notify := ntfy.New(cfg.Ntfy.Topic) - hue := hue.New(cfg.Hue.IP, cfg.Hue.Token) - // Devices that we control and expose to google home - home := home.New(cfg.Google.Username, cfg.Google.Credentials, client) + zigbee.DevicesHandler(client, home) - // Setup presence system p := presence.New(client, hue, notify, home) defer p.Delete(client) - r := mux.NewRouter() - r.HandleFunc("/assistant", home.Service.FullfillmentHandler) - - // Register computers - for name, info := range cfg.Computer { - home.AddDevice(wol.NewComputer(info.MACAddress, name, info.Url)) + 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) - // Register all kasa devies - for name, ip := range cfg.Kasa.Outlets { - home.AddDevice(kasa.NewOutlet(name, ip)) - } - - // Setup handler that automatically registers and updates all zigbee devices - zigbee.DevicesHandler(client, home) - automation.RegisterAutomations(client, hue, notify, home) + automation.RegisterAutomations(automationClient, hue, notify, home) addr := ":8090" srv := http.Server{