More refactoring, moved kettle auto off out of the kettle implementation and into a seperate automation
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Dreaded_X 2022-11-19 04:36:40 +01:00
parent 20e7e830a6
commit 5aefcf0157
Signed by: Dreaded_X
GPG Key ID: 76BDEC4E165D8AD9
13 changed files with 99 additions and 68 deletions

View File

@ -1,5 +1,5 @@
.git/ .git/
storage/ storage/
automation app
.env .env
tmp/ tmp/

View File

@ -11,4 +11,5 @@ import (
func RegisterAutomations(client paho.Client, hue *hue.Hue, notify *ntfy.Notify, home *home.Home) { func RegisterAutomations(client paho.Client, hue *hue.Hue, notify *ntfy.Notify, home *home.Home) {
presenceAutomation(client, hue, notify, home) presenceAutomation(client, hue, notify, home)
mixerAutomation(client, home) mixerAutomation(client, home)
kettleAutomation(client, home)
} }

50
automation/kettle.go Normal file
View File

@ -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)
}
}()
}

View File

@ -3,7 +3,6 @@ package automation
import ( import (
"automation/device" "automation/device"
"automation/home" "automation/home"
"automation/integration/kasa"
"encoding/json" "encoding/json"
"log" "log"
@ -12,12 +11,12 @@ import (
func mixerAutomation(client paho.Client, home *home.Home) { func mixerAutomation(client paho.Client, home *home.Home) {
var handler paho.MessageHandler = func(client paho.Client, msg paho.Message) { 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 { if err != nil {
log.Println(err) log.Println(err)
return 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 { if err != nil {
log.Println(err) log.Println(err)
return return

View File

@ -9,8 +9,6 @@ import (
"google.golang.org/api/homegraph/v1" "google.golang.org/api/homegraph/v1"
"google.golang.org/api/option" "google.golang.org/api/option"
paho "github.com/eclipse/paho.mqtt.golang"
) )
type Home struct { type Home struct {
@ -21,7 +19,7 @@ type Home struct {
} }
// Auto populate and update the device list // 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)} home := &Home{Username: username, Devices: make(map[device.InternalName]device.Basic)}
homegraphService, err := homegraph.NewService(context.Background(), option.WithCredentialsJSON(credentials)) homegraphService, err := homegraph.NewService(context.Background(), option.WithCredentialsJSON(credentials))

View File

@ -13,6 +13,7 @@ import (
type DeviceInterface interface { type DeviceInterface interface {
device.Basic device.Basic
IsGoogleDevice()
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)

View File

@ -1,6 +1,7 @@
package kasa package kasa
import ( import (
"automation/device"
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"encoding/json" "encoding/json"
@ -12,6 +13,9 @@ import (
// https://www.softscheck.com/en/blog/tp-link-reverse-engineering/ // https://www.softscheck.com/en/blog/tp-link-reverse-engineering/
type Device interface { type Device interface {
device.Basic
IsKasaDevice()
GetIP() string GetIP() string
} }

View File

@ -17,6 +17,9 @@ func NewOutlet(name device.InternalName, ip string) *Outlet {
// kasa.Device // kasa.Device
var _ Device = (*Outlet)(nil) var _ Device = (*Outlet)(nil)
func (*Outlet) IsKasaDevice() {}
// kasa.Device
func (o *Outlet) GetIP() string { func (o *Outlet) GetIP() string {
return o.ip return o.ip
} }

View File

@ -35,6 +35,9 @@ func (c *computer) GetID() device.InternalName {
// google.DeviceInterface // google.DeviceInterface
var _ google.DeviceInterface = (*computer)(nil) var _ google.DeviceInterface = (*computer)(nil)
func (*computer) IsGoogleDevice() {}
// google.DeviceInterface
func (c *computer) Sync() *google.Device { func (c *computer) Sync() *google.Device {
device := google.NewDevice(c.GetID().String(), google.TypeScene) device := google.NewDevice(c.GetID().String(), google.TypeScene)
device.AddSceneTrait(false) device.AddSceneTrait(false)

View File

@ -30,6 +30,8 @@ func DevicesHandler(client paho.Client, home *home.Home) {
} }
// Send sync request // 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) home.Service.RequestSync(context.Background(), home.Username)
} }

View File

@ -20,21 +20,12 @@ type kettle struct {
updated chan bool updated chan bool
timerLength time.Duration
timer *time.Timer
stop chan interface{}
isOn bool isOn bool
online bool online bool
} }
func NewKettle(info Info, client paho.Client, service *google.Service) *kettle { 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 := &kettle{info: info, client: client, service: service, updated: make(chan bool, 1)}
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()) 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{ k.service.ReportState(context.Background(), id, map[string]google.DeviceState{
id: k.getState(), 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 { func (k *kettle) getState() google.DeviceState {
@ -94,21 +64,20 @@ func (k *kettle) getState() google.DeviceState {
// zigbee.Device // zigbee.Device
var _ Device = (*kettle)(nil) var _ Device = (*kettle)(nil)
func (k *kettle) Delete() { func (k *kettle) IsZigbeeDevice() {}
k.stop <- struct{}{}
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()) log.Println(token.Error())
} }
} }
func (k *kettle) IsZigbeeDevice() bool {
return true
}
// google.DeviceInterface // google.DeviceInterface
var _ google.DeviceInterface = (*kettle)(nil) var _ google.DeviceInterface = (*kettle)(nil)
func (*kettle) IsGoogleDevice() {}
// google.DeviceInterface
func (k *kettle) Sync() *google.Device { func (k *kettle) Sync() *google.Device {
device := google.NewDevice(k.GetID().String(), google.TypeKettle) device := google.NewDevice(k.GetID().String(), google.TypeKettle)
device.AddOnOffTrait(false, false) device.AddOnOffTrait(false, false)

View File

@ -14,7 +14,6 @@ type Info struct {
type Device interface { type Device interface {
device.Basic device.Basic
// This function only exists to make this interface unique IsZigbeeDevice()
IsZigbeeDevice() bool
Delete() Delete()
} }

42
main.go
View File

@ -26,7 +26,21 @@ func main() {
cfg := config.Get() 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 := paho.NewClientOptions().AddBroker(fmt.Sprintf("%s:%d", cfg.MQTT.Host, cfg.MQTT.Port))
opts.SetClientID(cfg.MQTT.ClientID) opts.SetClientID(cfg.MQTT.ClientID)
opts.SetUsername(cfg.MQTT.Username) opts.SetUsername(cfg.MQTT.Username)
@ -38,32 +52,20 @@ func main() {
panic(token.Error()) panic(token.Error())
} }
defer client.Disconnect(250) 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 zigbee.DevicesHandler(client, home)
home := home.New(cfg.Google.Username, cfg.Google.Credentials, client)
// Setup presence system
p := presence.New(client, hue, notify, home) p := presence.New(client, hue, notify, home)
defer p.Delete(client) defer p.Delete(client)
r := mux.NewRouter() opts.SetClientID(fmt.Sprintf("%s-2", cfg.MQTT.ClientID))
r.HandleFunc("/assistant", home.Service.FullfillmentHandler) automationClient := paho.NewClient(opts)
if token := automationClient.Connect(); token.Wait() && token.Error() != nil {
// Register computers panic(token.Error())
for name, info := range cfg.Computer {
home.AddDevice(wol.NewComputer(info.MACAddress, name, info.Url))
} }
defer automationClient.Disconnect(250)
// Register all kasa devies automation.RegisterAutomations(automationClient, hue, notify, home)
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)
addr := ":8090" addr := ":8090"
srv := http.Server{ srv := http.Server{