Finished major refactor
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2022-11-19 03:50:12 +01:00
parent 01b2d492ba
commit c091ad0782
17 changed files with 512 additions and 599 deletions

View File

@@ -1,72 +0,0 @@
package device
import (
"automation/integration/google"
"log"
"net/http"
)
type computer struct {
macAddress string
name string
room string
url string
}
func NewComputer(macAddress string, name string, room string, url string) *computer {
c := &computer{macAddress: macAddress, name: name, room: room}
return c
}
func (c *computer) Sync() *google.Device {
device := google.NewDevice(c.GetID(), google.TypeScene)
device.AddSceneTrait(false)
device.Name = google.DeviceName{
DefaultNames: []string{
"Computer",
},
Name: c.name,
}
device.RoomHint = c.room
return device
}
func (c *computer) Query() google.DeviceState {
state := google.NewDeviceState(true)
state.Status = google.StatusSuccess
return state
}
func (c *computer) Execute(execution google.Execution, updateState *google.DeviceState) (string, bool) {
errCode := ""
switch execution.Name {
case google.CommandActivateScene:
c.SetState(!execution.ActivateScene.Deactivate)
default:
errCode = "actionNotAvailable"
log.Printf("Command (%s) not supported\n", execution.Name)
}
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
}
}

43
device/device.go Normal file
View 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
View 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)
}

View File

@@ -1,204 +0,0 @@
package device
import (
"automation/integration/google"
"encoding/json"
"fmt"
"log"
"strings"
"time"
paho "github.com/eclipse/paho.mqtt.golang"
)
type kettle struct {
info DeviceInfo
client paho.Client
updated chan bool
timerLength time.Duration
timer *time.Timer
stop chan interface{}
isOn bool
online bool
}
func (k *kettle) getState() google.DeviceState {
return google.NewDeviceState(k.online).RecordOnOff(k.isOn)
}
func (k *kettle) stateHandler(client paho.Client, msg paho.Message) {
var payload struct {
State string `json:"state"`
}
json.Unmarshal(msg.Payload(), &payload)
// Update the internal state
k.isOn = payload.State == "ON"
k.online = true
// Notify that the state has updated
for len(k.updated) > 0 {
<- k.updated
}
k.updated <- true
// Notify google of the updated state
// @TODO Fix this
// id := k.GetID()
// s.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) Delete() {
// The the timer function that it needs to stop
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
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) Sync() *google.Device {
device := google.NewDevice(k.GetID(), google.TypeKettle)
device.AddOnOffTrait(false, false)
s := strings.Split(k.info.FriendlyName, "/")
room := ""
name := s[0]
if len(s) > 1 {
room = s[0]
name = s[1]
}
room = strings.Title(room)
name = strings.Title(name)
device.Name = google.DeviceName{
DefaultNames: []string{
"Kettle",
},
Name: name,
}
// @TODO Fix reporting
// device.WillReportState = true
device.WillReportState = true
if len(name) > 1 {
device.RoomHint = room
}
device.DeviceInfo = google.DeviceInfo{
Manufacturer: k.info.Manufacturer,
Model: k.info.ModelID,
SwVersion: k.info.SoftwareBuildID,
}
return device
}
func (k *kettle) Query() google.DeviceState {
// We just report out internal representation as it should always match the actual state
state := k.getState()
// No /get needed
if k.online {
state.Status = google.StatusSuccess
} else {
state.Status = google.StatusOffline
}
return state
}
func (k *kettle) Execute(execution google.Execution, updatedState *google.DeviceState) (string, bool) {
errCode := ""
switch execution.Name {
case google.CommandOnOff:
// Clear the updated channel
for len(k.updated) > 0 {
<- k.updated
}
k.SetState(execution.OnOff.On)
// Start timeout timer
timer := time.NewTimer(time.Second)
// Wait for the update or timeout
select {
case <- k.updated:
updatedState.RecordOnOff(k.isOn)
case <- timer.C:
// If we do not get a response in time mark the device as offline
log.Println("Device did not respond, marking as offline")
k.online = false
}
default:
// @TODO Should probably move the error codes to a enum
errCode = "actionNotAvailable"
log.Printf("Command (%s) not supported\n", execution.Name)
}
return errCode, k.online
}
func (k *kettle) GetID() string {
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) {
msg := "OFF"
if state {
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 {
log.Println(token.Error())
}
}

View File

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