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": NewVoltMeter, "current": NewAmpMeter, } // 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 { dev.setup() 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) }