This commit is contained in:
@@ -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
43
device/device.go
Normal 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
33
device/internal_name.go
Normal 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)
|
||||
}
|
||||
|
||||
204
device/kettle.go
204
device/kettle.go
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user