This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package google
|
||||
|
||||
import (
|
||||
"automation/device"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
@@ -10,10 +11,11 @@ import (
|
||||
)
|
||||
|
||||
type DeviceInterface interface {
|
||||
device.Basic
|
||||
|
||||
Sync() *Device
|
||||
Query() DeviceState
|
||||
Execute(execution Execution, updatedState *DeviceState) (errCode string, online bool)
|
||||
GetID() string
|
||||
}
|
||||
|
||||
// https://developers.google.com/assistant/smarthome/reference/intent/sync
|
||||
|
||||
@@ -5,13 +5,16 @@ import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
)
|
||||
|
||||
// This implementation is based on:
|
||||
// https://www.softscheck.com/en/blog/tp-link-reverse-engineering/
|
||||
|
||||
type Device interface {
|
||||
GetIP() string
|
||||
}
|
||||
|
||||
func encrypt(data []byte) []byte {
|
||||
var key byte = 171
|
||||
buf := new(bytes.Buffer)
|
||||
@@ -45,18 +48,8 @@ func decrypt(data []byte) ([]byte, error) {
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
|
||||
type Kasa struct {
|
||||
name string
|
||||
ip string
|
||||
}
|
||||
|
||||
func New(name string, ip string) *Kasa {
|
||||
return &Kasa{name, ip}
|
||||
}
|
||||
|
||||
func (kasa *Kasa) sendCmd(cmd cmd) (reply, error) {
|
||||
con, err := net.Dial("tcp", fmt.Sprintf("%s:9999", kasa.ip))
|
||||
func sendCmd(kasa Device, cmd cmd) (reply, error) {
|
||||
con, err := net.Dial("tcp", fmt.Sprintf("%s:9999", kasa.GetIP()))
|
||||
if err != nil {
|
||||
return reply{}, err
|
||||
}
|
||||
@@ -93,45 +86,3 @@ func (kasa *Kasa) sendCmd(cmd cmd) (reply, error) {
|
||||
return reply, err
|
||||
}
|
||||
|
||||
func (kasa *Kasa) SetState(on bool) {
|
||||
var cmd cmd
|
||||
cmd.System.SetRelayState = &SetRelayState{State: 0}
|
||||
if on {
|
||||
cmd.System.SetRelayState.State = 1
|
||||
}
|
||||
|
||||
reply, err := kasa.sendCmd(cmd)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
if reply.System.SetRelayState.ErrCode != 0 {
|
||||
log.Printf("Failed to set relay state, error: %d\n", reply.System.SetRelayState.ErrCode)
|
||||
}
|
||||
}
|
||||
|
||||
func (kasa *Kasa) GetState() bool {
|
||||
cmd := cmd{}
|
||||
|
||||
cmd.System.GetSysinfo = &GetSysinfo{}
|
||||
|
||||
reply, err := kasa.sendCmd(cmd)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false
|
||||
}
|
||||
|
||||
if reply.System.GetSysinfo.ErrCode != 0 {
|
||||
log.Printf("Failed to set relay state, error: %d\n", reply.System.GetSysinfo.ErrCode)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return reply.System.GetSysinfo.RelayState == 1
|
||||
}
|
||||
|
||||
func (kasa *Kasa) GetName() string {
|
||||
return kasa.name
|
||||
}
|
||||
|
||||
|
||||
68
integration/kasa/outlet.go
Normal file
68
integration/kasa/outlet.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package kasa
|
||||
|
||||
import (
|
||||
"automation/device"
|
||||
"log"
|
||||
)
|
||||
|
||||
type Outlet struct {
|
||||
name device.InternalName
|
||||
ip string
|
||||
}
|
||||
|
||||
|
||||
func NewOutlet(name device.InternalName, ip string) *Outlet {
|
||||
return &Outlet{name, ip}
|
||||
}
|
||||
|
||||
// kasa.Device
|
||||
var _ Device = (*Outlet)(nil)
|
||||
func (o *Outlet) GetIP() string {
|
||||
return o.ip
|
||||
}
|
||||
|
||||
// device.Basic
|
||||
var _ device.Basic = (*Outlet)(nil)
|
||||
func (o *Outlet) GetID() device.InternalName {
|
||||
return o.name
|
||||
}
|
||||
|
||||
// device.OnOff
|
||||
var _ device.OnOff = (*Outlet)(nil)
|
||||
func (o *Outlet) SetOnOff(on bool) {
|
||||
var cmd cmd
|
||||
cmd.System.SetRelayState = &SetRelayState{State: 0}
|
||||
if on {
|
||||
cmd.System.SetRelayState.State = 1
|
||||
}
|
||||
|
||||
reply, err := sendCmd(o, cmd)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
if reply.System.SetRelayState.ErrCode != 0 {
|
||||
log.Printf("Failed to set relay state, error: %d\n", reply.System.SetRelayState.ErrCode)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Outlet) GetOnOff() bool {
|
||||
cmd := cmd{}
|
||||
|
||||
cmd.System.GetSysinfo = &GetSysinfo{}
|
||||
|
||||
reply, err := sendCmd(o, cmd)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false
|
||||
}
|
||||
|
||||
if reply.System.GetSysinfo.ErrCode != 0 {
|
||||
log.Printf("Failed to set relay state, error: %d\n", reply.System.GetSysinfo.ErrCode)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return reply.System.GetSysinfo.RelayState == 1
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package mqtt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
paho "github.com/eclipse/paho.mqtt.golang"
|
||||
)
|
||||
|
||||
// This is the default message handler, it just prints out the topic and message
|
||||
var defaultHandler paho.MessageHandler = func(client paho.Client, msg paho.Message) {
|
||||
fmt.Printf("TOPIC: %s\n", msg.Topic())
|
||||
fmt.Printf("MSG: %s\n", msg.Payload())
|
||||
}
|
||||
|
||||
func New(host string, port int, clientID string, username string, password string) paho.Client {
|
||||
opts := paho.NewClientOptions().AddBroker(fmt.Sprintf("%s:%d", host, port))
|
||||
opts.SetClientID(clientID)
|
||||
opts.SetDefaultPublishHandler(defaultHandler)
|
||||
opts.SetUsername(username)
|
||||
opts.SetPassword(password)
|
||||
opts.SetOrderMatters(false)
|
||||
|
||||
client := paho.NewClient(opts)
|
||||
if token := client.Connect(); token.Wait() && token.Error() != nil {
|
||||
panic(token.Error())
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func Delete(m paho.Client) {
|
||||
if token := m.Unsubscribe("automation/presence/+"); token.Wait() && token.Error() != nil {
|
||||
fmt.Println(token.Error())
|
||||
}
|
||||
|
||||
m.Disconnect(250)
|
||||
}
|
||||
@@ -11,7 +11,6 @@ type Notify struct {
|
||||
}
|
||||
|
||||
func (n *Notify) Presence(home bool) {
|
||||
// @TODO Maybe add list the devices that are home currently?
|
||||
var description string
|
||||
var actions string
|
||||
if home {
|
||||
|
||||
74
integration/wol/computer.go
Normal file
74
integration/wol/computer.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package wol
|
||||
|
||||
import (
|
||||
"automation/device"
|
||||
"automation/integration/google"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type computer struct {
|
||||
macAddress string
|
||||
name device.InternalName
|
||||
url string
|
||||
}
|
||||
|
||||
func NewComputer(macAddress string, name device.InternalName, url string) *computer {
|
||||
c := &computer{macAddress: macAddress, name: name}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *computer) Activate(state bool) {
|
||||
if state {
|
||||
http.Get(c.url)
|
||||
} else {
|
||||
// Scene does not implement this
|
||||
}
|
||||
}
|
||||
|
||||
// device.Basic
|
||||
var _ device.Basic = (*computer)(nil)
|
||||
func (c *computer) GetID() device.InternalName {
|
||||
return device.InternalName(c.name)
|
||||
}
|
||||
|
||||
// google.DeviceInterface
|
||||
var _ google.DeviceInterface = (*computer)(nil)
|
||||
func (c *computer) Sync() *google.Device {
|
||||
device := google.NewDevice(c.GetID().String(), google.TypeScene)
|
||||
device.AddSceneTrait(false)
|
||||
|
||||
device.Name = google.DeviceName{
|
||||
DefaultNames: []string{
|
||||
"Computer",
|
||||
},
|
||||
Name: c.GetID().Name(),
|
||||
}
|
||||
device.RoomHint = c.GetID().Room()
|
||||
|
||||
return device
|
||||
}
|
||||
|
||||
// google.DeviceInterface
|
||||
func (c *computer) Query() google.DeviceState {
|
||||
state := google.NewDeviceState(true)
|
||||
state.Status = google.StatusSuccess
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
// google.DeviceInterface
|
||||
func (c *computer) Execute(execution google.Execution, updateState *google.DeviceState) (string, bool) {
|
||||
errCode := ""
|
||||
|
||||
switch execution.Name {
|
||||
case google.CommandActivateScene:
|
||||
c.Activate(!execution.ActivateScene.Deactivate)
|
||||
default:
|
||||
errCode = "actionNotAvailable"
|
||||
log.Printf("Command (%s) not supported\n", execution.Name)
|
||||
}
|
||||
|
||||
return errCode, true
|
||||
}
|
||||
39
integration/zigbee/devices.go
Normal file
39
integration/zigbee/devices.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package zigbee
|
||||
|
||||
import (
|
||||
"automation/device"
|
||||
"automation/home"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
paho "github.com/eclipse/paho.mqtt.golang"
|
||||
)
|
||||
|
||||
func DevicesHandler(client paho.Client, home *home.Home) {
|
||||
var handler paho.MessageHandler = func(client paho.Client, msg paho.Message) {
|
||||
var devices []Info
|
||||
json.Unmarshal(msg.Payload(), &devices)
|
||||
|
||||
for name, d := range device.GetDevices[Device](&home.Devices) {
|
||||
d.Delete()
|
||||
// Delete all zigbee devices from the device list
|
||||
delete(home.Devices, name)
|
||||
}
|
||||
|
||||
for _, d := range devices {
|
||||
switch d.Description {
|
||||
case "Kettle":
|
||||
kettle := NewKettle(d, client, home.Service)
|
||||
home.AddDevice(kettle)
|
||||
}
|
||||
}
|
||||
|
||||
// Send sync request
|
||||
home.Service.RequestSync(context.Background(), home.Username)
|
||||
}
|
||||
|
||||
if token := client.Subscribe("zigbee2mqtt/bridge/devices", 1, handler); token.Wait() && token.Error() != nil {
|
||||
log.Println(token.Error())
|
||||
}
|
||||
}
|
||||
209
integration/zigbee/kettle.go
Normal file
209
integration/zigbee/kettle.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package zigbee
|
||||
|
||||
import (
|
||||
"automation/device"
|
||||
"automation/integration/google"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
paho "github.com/eclipse/paho.mqtt.golang"
|
||||
)
|
||||
|
||||
type kettle struct {
|
||||
info Info
|
||||
|
||||
client paho.Client
|
||||
service *google.Service
|
||||
|
||||
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()
|
||||
|
||||
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) 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
|
||||
id := k.GetID().String()
|
||||
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 {
|
||||
return google.NewDeviceState(k.online).RecordOnOff(k.isOn)
|
||||
}
|
||||
|
||||
|
||||
// zigbee.Device
|
||||
var _ Device = (*kettle)(nil)
|
||||
func (k *kettle) Delete() {
|
||||
k.stop <- struct{}{}
|
||||
|
||||
if token := k.client.Subscribe(fmt.Sprintf("zigbee2mqtt/%s", k.info.FriendlyName), 1, k.stateHandler); token.Wait() && token.Error() != nil {
|
||||
log.Println(token.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (k *kettle) IsZigbeeDevice() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
// google.DeviceInterface
|
||||
var _ google.DeviceInterface = (*kettle)(nil)
|
||||
func (k *kettle) Sync() *google.Device {
|
||||
device := google.NewDevice(k.GetID().String(), google.TypeKettle)
|
||||
device.AddOnOffTrait(false, false)
|
||||
|
||||
device.Name = google.DeviceName{
|
||||
DefaultNames: []string{
|
||||
"Kettle",
|
||||
},
|
||||
Name: k.GetID().Name(),
|
||||
}
|
||||
|
||||
device.WillReportState = true
|
||||
room := k.GetID().Room()
|
||||
if len(room) > 1 {
|
||||
device.RoomHint = room
|
||||
}
|
||||
|
||||
device.DeviceInfo = google.DeviceInfo{
|
||||
Manufacturer: k.info.Manufacturer,
|
||||
Model: k.info.ModelID,
|
||||
SwVersion: k.info.SoftwareBuildID,
|
||||
}
|
||||
|
||||
return device
|
||||
}
|
||||
|
||||
// google.DeviceInterface
|
||||
func (k *kettle) Query() google.DeviceState {
|
||||
state := k.getState()
|
||||
if k.online {
|
||||
state.Status = google.StatusSuccess
|
||||
} else {
|
||||
state.Status = google.StatusOffline
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
// google.DeviceInterface
|
||||
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.SetOnOff(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
|
||||
}
|
||||
|
||||
// device.Base
|
||||
var _ device.Basic = (*kettle)(nil)
|
||||
func (k *kettle) GetID() device.InternalName {
|
||||
return k.info.FriendlyName
|
||||
}
|
||||
|
||||
// device.OnOff
|
||||
var _ device.OnOff = (*kettle)(nil)
|
||||
func (k *kettle) SetOnOff(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())
|
||||
}
|
||||
}
|
||||
|
||||
// device.OnOff
|
||||
func (k *kettle) GetOnOff() bool {
|
||||
return k.isOn
|
||||
}
|
||||
20
integration/zigbee/zigbee.go
Normal file
20
integration/zigbee/zigbee.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package zigbee
|
||||
|
||||
import "automation/device"
|
||||
|
||||
type Info struct {
|
||||
IEEEAdress string `json:"ieee_address"`
|
||||
FriendlyName device.InternalName `json:"friendly_name"`
|
||||
Description string `json:"description"`
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
ModelID string `json:"model_id"`
|
||||
SoftwareBuildID string `json:"software_build_id"`
|
||||
}
|
||||
|
||||
type Device interface {
|
||||
device.Basic
|
||||
|
||||
// This function only exists to make this interface unique
|
||||
IsZigbeeDevice() bool
|
||||
Delete()
|
||||
}
|
||||
Reference in New Issue
Block a user