-
KERDREUX Jerome authored
Used for new devices type
KERDREUX Jerome authoredUsed for new devices type
z2m.go 6.63 KiB
package main
import (
"encoding/json"
"fmt"
"log/slog"
"strings"
mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/jedib0t/go-pretty/v6/table"
"gitlab.imt-atlantique.fr/xaal/code/go/core/uuid"
)
const (
AccessLevelR AccessLevel = 1
AccessLevelW AccessLevel = 2
AccessLevelRW AccessLevel = 3
AccessLevelRN AccessLevel = 5
AccessLevelRWN AccessLevel = 7
)
type (
AccessLevel int
)
// JSON structures for the z2m device from /bridge/devices
type Z2MDevice struct {
Gateway *Gateway
Type string `json:"type"`
IeeeAddress string `json:"ieee_address"`
SwBuildID string `json:"software_build_id"`
FriendlyName string `json:"friendly_name"`
Definition struct {
Vendor string `json:"vendor"`
Model string `json:"model"`
Exposes []Expose `json:"exposes"`
} `json:"definition"`
XAALDevices []XAALDeviceInterface
}
type Expose struct {
Name string `json:"name"`
Type string `json:"type"`
Unit string `json:"unit,omitempty"`
Features []Feature `json:"features,omitempty"`
Values []string `json:"values,omitempty"`
Access int `json:"access,omitempty"`
}
type Feature struct {
Name string `json:"name"`
Type string `json:"type"`
Property string `json:"property,omitempty"`
Unit string `json:"unit,omitempty"`
Access int `json:"access,omitempty"`
}
// =============================================================================
// Z2MDevice API
// =============================================================================
// returns the topic for the device
func (zDev *Z2MDevice) getTopic() string {
return zDev.Gateway.Config.topic + "/" + zDev.FriendlyName
}
// return the expose with the given name
func (zDev *Z2MDevice) GetExpose(name string) *Expose {
for _, expose := range zDev.Definition.Exposes {
if expose.Name == name {
return &expose
}
}
return nil
}
// updates the xAAL device with the MQTT message
func (zDev *Z2MDevice) HandleMessage(msg mqtt.Message) {
var data map[string]interface{}
err := json.Unmarshal(msg.Payload(), &data)
if err != nil {
slog.Error("Error decoding JSON", "err", err, "topic", msg.Topic(), "data", msg.Payload())
} else {
slog.Debug("Updating device:", "name", zDev.FriendlyName)
for _, dev := range zDev.XAALDevices {
dev.update(data)
}
}
}
// creates new xAAL devices from a bridge device
func (zDev *Z2MDevice) FindXAALDevices(gw *Gateway) {
// There is a trick here. We call FindXAALDevices w/ gw argument, but the Gateway is only set
// in the AddZDevice method. This is not mandatory but avoid to have Gateway at random place
baseAddr := gw.Config.baseAddr
ieeeAddr, _ := hexStringToInteger(zDev.IeeeAddress)
baseAddr, _ = baseAddr.Add(int64(ieeeAddr))
for i, expose := range zDev.Definition.Exposes {
addr, _ := baseAddr.Add(int64(i))
grpAdd, _ := baseAddr.Add(int64(0xff))
var dev XAALDeviceInterface
deviceMap := map[string]func(uuid.UUID, *Z2MDevice, *Expose) XAALDeviceInterface{
"contact": NewContact,
"temperature": NewThermometer,
"humidity": NewHygrometer,
"linkquality": NewLinkQuality,
"battery": NewBattery,
"power": NewPowerMeter,
"action": NewButtonRemote,
"switch": NewPowerRelay,
"light": NewLamp,
"occupancy": NewMotion,
"illuminance": NewLuxMeter,
"voltage": NewDebugDevice,
}
// Search a matching expose name
if createFunc, ok := deviceMap[expose.Name]; ok {
dev = createFunc(addr, zDev, &expose)
}
// Search a matching expose type
if createFunc, ok := deviceMap[expose.Type]; ok {
dev = createFunc(addr, zDev, &expose)
}
if dev != nil {
zDev.XAALDevices = append(zDev.XAALDevices, dev)
xaalDev := dev.GetXAALDevice()
xaalDev.GroupID = grpAdd
}
}
}
//==============================================================================
// MQTT API
//==============================================================================
// Publish the payload to the right topic
func (zDev *Z2MDevice) Publish(topic string, payload interface{}) {
topic = zDev.getTopic() + "/" + topic
slog.Debug("Sending", "topic", topic, "payload", payload)
client := zDev.Gateway.Client
if token := client.Publish(topic, 0, false, payload); token.Wait() && token.Error() != nil {
slog.Error("PUBLISH Error", ":", token.Error())
}
}
// Publish the device wanted state
func (zDev *Z2MDevice) Set(payload interface{}) {
zDev.Publish("set", payload)
}
// Check if the device is available, Z2M will send an availability message
// if this enable in Z2M config
func (zDev *Z2MDevice) Available() {
zDev.Publish("availability", `{"state": "online"}`)
}
// Ask for a device to dump its state. Z2M doesn't support this but the
// dump-state-ext.js provide this feature
func (zDev *Z2MDevice) Sync() {
zDev.Publish("dump", "{}")
}
func (zDev *Z2MDevice) Dump() {
tab := table.NewWriter()
tab.SetTitle("Def:" + zDev.FriendlyName)
tab.SetStyle(table.StyleRounded)
tab.AppendRow(table.Row{"IeeeAddr", zDev.IeeeAddress})
tab.AppendRow(table.Row{"Vendor", zDev.Definition.Vendor})
tab.AppendRow(table.Row{"Model", zDev.Definition.Model})
tab.AppendRow(table.Row{"Type", zDev.Type})
fmt.Println(tab.Render())
if len(zDev.Definition.Exposes) > 0 {
expTab := table.NewWriter()
expTab.SetTitle("Exp:" + zDev.FriendlyName)
expTab.SetStyle(table.StyleRounded)
expTab.AppendHeader(table.Row{"Name", "Type", "Acc", "Unit", "Values", "Features: Name[Type]-Acc-(Unit){Property}"})
for _, expose := range zDev.Definition.Exposes {
values := ""
if len(expose.Values) > 0 {
values = strings.Join(expose.Values, "\n")
}
features := ""
if len(expose.Features) > 0 {
for _, feature := range expose.Features {
features += fmt.Sprintf("- %s[%s]-%s-(%s){%s}\n", feature.Name, feature.Type, AccessLevel(feature.Access), feature.Unit, feature.Property)
}
features = strings.TrimSuffix(features, "\n")
}
expTab.AppendRow(table.Row{expose.Name, expose.Type, AccessLevel(expose.Access), expose.Unit, values, features})
}
fmt.Println(expTab.Render())
}
}
// =============================================================================
// Expose
// =============================================================================
func (e *Expose) GetFeature(name string) *Feature {
for _, feature := range e.Features {
if feature.Name == name {
return &feature
}
}
return nil
}
// =============================================================================
// Helpers
// =============================================================================
func (l AccessLevel) String() string {
return [...]string{"--", "R", "W", "RW", "Err", "RN", "Err", "RWN"}[l]
}
func (l AccessLevel) EnumIndex() int {
return int(l)
}