package main import ( "encoding/json" "fmt" "log/slog" "slices" "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 ) var ignoredTopics = []string{ mqttTopic + "/bridge/groups", mqttTopic + "/bridge/definitions", mqttTopic + "/bridge/extensions", mqttTopic + "/bridge/info", mqttTopic + "/bridge/state", mqttTopic + "/bridge/logging", mqttTopic + "/bridge/config", } // JSON structures for the z2m device from /bridge/devices type Z2MDevice struct { Definition struct { Vendor string `json:"vendor"` Model string `json:"model"` Exposes []Expose `json:"exposes"` } `json:"definition"` Type string `json:"type"` IeeeAddress string `json:"ieee_address"` SwBuildID string `json:"software_build_id"` FriendlyName string `json:"friendly_name"` 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 mqttTopic + "/" + 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) } else { slog.Info("Updating device:", "name", zDev.FriendlyName) for _, dev := range zDev.XAALDevices { dev.update(data) } } } // creates new xAAL devices from a bridge device func (zDev *Z2MDevice) setupXAALDevices(gw *Gateway) { // TODO: Handle errors baseAddr := gw.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) XAALDeviceInterface{ "contact": NewContact, "temperature": NewThermometer, "humidity": NewHygrometer, "linkquality": NewLinkQuality, "battery": NewBattery, "power": NewPowerMeter, "action": NewButtonRemote, } if createFunc, ok := deviceMap[expose.Name]; ok { dev = createFunc(addr, zDev) } else if expose.Type == "switch" { property := "state" for _, exp := range expose.Features { if strings.Contains(exp.Property, "state") { property = exp.Property break } } dev = NewPowerRelay(addr, zDev, property) } else if expose.Type == "light" { // type := NewLamp // for _, exp := range expose.Features { // } dev = NewLamp(addr, zDev, &expose) } if dev != nil { zDev.XAALDevices = append(zDev.XAALDevices, dev) xaalDev := dev.getXAALDevice() xaalDev.GroupID = grpAdd gw.engine.AddDevice(xaalDev) } } } //============================================================================== // 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 := GetGW().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) } // jsonParseDevices parses the bridge/devices json and creates new xAAL devices // if they don't exist func jsonParseDevices(jsonData []byte) { var devices []Z2MDevice err := json.Unmarshal([]byte(jsonData), &devices) if err != nil { slog.Error("Error decoding JSON", "err", err) } gw := GetGW() for _, zDev := range devices { known := gw.GetZDevice(zDev.FriendlyName) if known != nil { continue } zDev.dump() zDev.setupXAALDevices(gw) gw.AddZDevice(&zDev) zDev.Sync() // zDev.Available() } } // mqttPublishHander handles all incoming MQTT messages // If the topic is /bridge/devices it will parse the json and create new devices // Else it will find the device with the topic and call the mqttDeviceHandler func mqttPublishHander(client MQTT.Client, msg MQTT.Message) { // we ignore some topics if slices.Contains(ignoredTopics, msg.Topic()) { return } slog.Debug("Received message on", "topic", msg.Topic()) // Is it devices definitions ? if msg.Topic() == mqttTopic+"/bridge/devices" { jsonParseDevices(msg.Payload()) } else { dev := GetGW().GetZDeviceByTopic(msg.Topic()) mqttDumpMsg(msg) if dev != nil { dev.HandleMessage(msg) } } }