package main import ( "fmt" "log/slog" "github.com/lucasb-eyer/go-colorful" "gitlab.imt-atlantique.fr/xaal/code/go/core/schemas" "gitlab.imt-atlantique.fr/xaal/code/go/core/uuid" "gitlab.imt-atlantique.fr/xaal/code/go/core/xaal" ) // This is a xaal.Device w/ Z2MDevice and Expose type XAALDevice struct { *xaal.Device Z2MDevice *Z2MDevice Expose *Expose } type Contact struct { XAALDevice } type Thermometer struct { XAALDevice } type Hygrometer struct { XAALDevice } type LinkQuality struct { XAALDevice } type Battery struct { XAALDevice } type PowerRelay struct { XAALDevice } type PowerMeter struct { XAALDevice } type Lamp struct { XAALDevice } type ButtonRemote struct { XAALDevice } type Motion struct { XAALDevice } type LuxMeter struct { XAALDevice } type DebugDevice struct { XAALDevice } type XAALDeviceInterface interface { update(map[string]interface{}) GetXAALDevice() *xaal.Device } func (dev *XAALDevice) setup() { zDev := dev.Z2MDevice dev.VendorID = zDev.Definition.Vendor dev.ProductID = zDev.Definition.Model dev.HWID = zDev.IeeeAddress dev.Version = zDev.SwBuildID dev.Info = "z2m:" + zDev.Type + ":" + zDev.FriendlyName } func (dev *XAALDevice) GetXAALDevice() *xaal.Device { return dev.Device } // ============================================================================= // Contact // ============================================================================= func NewContact(addr uuid.UUID, zDev *Z2MDevice, exp *Expose) XAALDeviceInterface { dev := &Contact{XAALDevice{schemas.NewContact(addr), zDev, exp}} dev.setup() return dev } func (dev *Contact) update(payload map[string]interface{}) { value, exists := payload[dev.Expose.Name].(bool) if exists { dev.GetAttribute("detected").SetValue(!value) } } // ============================================================================= // Thermometer // ============================================================================= func NewThermometer(addr uuid.UUID, zDev *Z2MDevice, exp *Expose) XAALDeviceInterface { dev := &Thermometer{XAALDevice{schemas.NewThermometer(addr), zDev, exp}} dev.setup() return dev } func (dev *Thermometer) update(payload map[string]interface{}) { value, exists := payload[dev.Expose.Name].(float64) if exists { dev.GetAttribute("temperature").SetValue(value) } } // ============================================================================= // Hygrometer // ============================================================================= func NewHygrometer(addr uuid.UUID, zDev *Z2MDevice, exp *Expose) XAALDeviceInterface { dev := &Hygrometer{XAALDevice{schemas.NewHygrometer(addr), zDev, exp}} dev.setup() return dev } func (dev *Hygrometer) update(payload map[string]interface{}) { value, exists := payload[dev.Expose.Name].(float64) if exists { dev.GetAttribute("humidity").SetValue(value) } } // ============================================================================= // LinkQuality // ============================================================================= func NewLinkQuality(addr uuid.UUID, zDev *Z2MDevice, exp *Expose) XAALDeviceInterface { dev := &LinkQuality{XAALDevice{schemas.NewLinkquality(addr), zDev, exp}} dev.GetAttribute("level").Value = 0 // override type to int dev.setup() dev.RemoveAttribute("devices") dev.UnsupportedAttributes = []string{"devices"} return dev } func (dev *LinkQuality) update(payload map[string]interface{}) { value, err := convertToInt(payload[dev.Expose.Name]) if err == nil { value = value * 100 / 255 oldValue := dev.GetAttribute("level").Value.(int) if value > oldValue+5 || value < oldValue-5 { dev.GetAttribute("level").SetValue(value) } } } // ============================================================================= // Battery // ============================================================================= func NewBattery(addr uuid.UUID, zDev *Z2MDevice, exp *Expose) XAALDeviceInterface { dev := &Battery{XAALDevice{schemas.NewBattery(addr), zDev, exp}} dev.setup() dev.RemoveAttribute("devices") dev.UnsupportedAttributes = []string{"devices"} return dev } func (dev *Battery) update(payload map[string]interface{}) { value, exists := payload[dev.Expose.Name].(float64) if exists { dev.GetAttribute("level").SetValue(value) } } // ============================================================================= // PowerRelay // ============================================================================= func NewPowerRelay(addr uuid.UUID, zDev *Z2MDevice, exp *Expose) XAALDeviceInterface { dev := &PowerRelay{XAALDevice{schemas.NewPowerrelayToggle(addr), zDev, exp}} dev.SetMethod("turn_on", dev.turnOn) dev.SetMethod("turn_off", dev.turnOff) dev.SetMethod("toggle", dev.toggle) dev.setup() return dev } func (dev *PowerRelay) update(payload map[string]interface{}) { // TODO: Handle missing state property, this can be done in constructor state := dev.Expose.GetFeature("state") value, exists := payload[state.Property].(string) if exists { power := dev.GetAttribute("power") switch value { case "ON": power.SetValue(true) case "OFF": power.SetValue(false) } } } func (dev *PowerRelay) SetState(value string) { state := dev.Expose.GetFeature("state") body := fmt.Sprintf(`{"%s": "%s"}`, state.Property, value) dev.Z2MDevice.Set(body) } func (dev *PowerRelay) turnOn(xaal.MessageBody) *xaal.MessageBody { dev.SetState("ON") return nil } func (dev *PowerRelay) turnOff(xaal.MessageBody) *xaal.MessageBody { dev.SetState("OFF") return nil } func (dev *PowerRelay) toggle(xaal.MessageBody) *xaal.MessageBody { dev.SetState("TOGGLE") return nil } // ============================================================================= // PowerMeter // ============================================================================= func NewPowerMeter(addr uuid.UUID, zDev *Z2MDevice, exp *Expose) XAALDeviceInterface { dev := &PowerMeter{XAALDevice{schemas.NewPowermeter(addr), zDev, exp}} dev.setup() dev.RemoveAttribute("devices") dev.UnsupportedAttributes = []string{"devices"} return dev } func (dev *PowerMeter) update(payload map[string]interface{}) { // power value, exists := payload[dev.Expose.Name].(float64) if exists { dev.GetAttribute("power").SetValue(value) } // energy value, exists = payload["energy"].(float64) if exists { dev.GetAttribute("energy").SetValue(value) } } // ============================================================================= // Lamp // ============================================================================= // The Lamp don't use the Expose.Names right now. Perhaps in future, if we find // some lamps w/ different exposes. This will make the code more complex. func NewLamp(addr uuid.UUID, zDev *Z2MDevice, exp *Expose) XAALDeviceInterface { // This is the default lamp device, we will change it's devType if needed dev := &Lamp{XAALDevice{schemas.NewLamp(addr), zDev, exp}} dev.SetMethod("turn_on", dev.turnOn) dev.SetMethod("turn_off", dev.turnOff) dev.SetMethod("toggle", dev.toggle) if exp.GetFeature("brightness") != nil { dev.DevType = "lamp.dimmer" dev.AddAttribute("brightness", 0) dev.AddMethod("set_brightness", dev.setBrightness) } if exp.GetFeature("color_temp") != nil { dev.DevType = "lamp.dimmer" dev.AddAttribute("white_temperature", 0) dev.AddMethod("set_white_temperature", dev.setWhiteTemperature) } if exp.GetFeature("color_xy") != nil { dev.DevType = "lamp.color" dev.AddAttribute("hsv", []float64{0, 0, 0}) dev.AddAttribute("mode", nil) dev.UnsupportedAttributes = []string{"scene"} dev.AddMethod("set_hsv", dev.setHSV) dev.AddMethod("set_mode", dev.setMode) } dev.setup() return dev } func (dev *Lamp) getMode() string { mode := dev.GetAttribute("mode") if mode != nil { return mode.Value.(string) } return "white" } func (dev *Lamp) update(payload map[string]interface{}) { // state state, exists := payload["state"].(string) if exists { light := dev.GetAttribute("light") switch state { case "ON": light.SetValue(true) case "OFF": light.SetValue(false) } } // color_mode color_mode, exists := payload["color_mode"].(string) if exists { mode := dev.GetAttribute("mode") // only color lamp have a mode if mode != nil { switch color_mode { case "xy": mode.SetValue("color") case "color_temp": mode.SetValue("white") } } } // brightness brightness, err := convertToInt(payload["brightness"]) if err == nil { brightness = brightness * 100 / 255 dev.GetAttribute("brightness").SetValue(int(brightness)) } // color_temp if dev.getMode() == "white" { // color_temp change when we are in color mode (looks like a z2m bug) // so we have to only update when we are in white mode. Without this check // the color_temp is wrong when you change the mode w/ setMode color_temp, err := convertToInt(payload["color_temp"]) if err == nil { dev.GetAttribute("white_temperature").SetValue(convertMired(color_temp)) } } // colors if dev.getMode() == "color" { color, exists := payload["color"].(map[string]interface{}) if exists { x, _ := convertToFloat64(color["x"]) y, _ := convertToFloat64(color["y"]) brigthness, _ := convertToFloat64(payload["brightness"]) value := xyToColor(x, y, brigthness) slog.Debug("color", "color", value.Hex(), "x", x, "y", y, "tmp", brigthness) hue, sat, val := value.Hsv() hue = roundToDecimal(hue, 1) sat = roundToDecimal(sat, 3) val = roundToDecimal(val, 3) dev.GetAttribute("hsv").SetValue([]float64{hue, sat, val}) } } } func (dev *Lamp) setState(value string) { state := dev.Expose.GetFeature("state") body := fmt.Sprintf(`{"%s": "%s"}`, state.Property, value) dev.Z2MDevice.Set(body) } func (dev *Lamp) turnOn(xaal.MessageBody) *xaal.MessageBody { dev.setState("ON") return nil } func (dev *Lamp) turnOff(xaal.MessageBody) *xaal.MessageBody { dev.setState("OFF") return nil } func (dev *Lamp) toggle(xaal.MessageBody) *xaal.MessageBody { dev.setState("TOGGLE") return nil } func (dev *Lamp) setBrightness(body xaal.MessageBody) *xaal.MessageBody { value, exists := body["brightness"] if exists { target, _ := convertToInt(value) dev.Z2MDevice.Set(fmt.Sprintf(`{"brightness": %d}`, target*255/100)) } return nil } func (dev *Lamp) setWhiteTemperature(body xaal.MessageBody) *xaal.MessageBody { value, exists := body["white_temperature"] if exists { target, _ := convertToInt(value) target = convertMired(target) dev.Z2MDevice.Set(fmt.Sprintf(`{"color_temp": %d}`, target)) } return nil } func (dev *Lamp) setHSV(body xaal.MessageBody) *xaal.MessageBody { value, exist := body["hsv"].([]interface{}) if exist { hue, _ := convertToFloat64(value[0]) sat, _ := convertToFloat64(value[1]) val, _ := convertToFloat64(value[2]) color := colorful.Hsv(hue, sat, val) slog.Debug("setHSV", "color", color.Hex(), "hue", hue, "sat", sat, "val", val) dev.Z2MDevice.Set(fmt.Sprintf(`{"color": { "hex": "%s" }}`, color.Hex())) } return nil } func (dev *Lamp) setMode(body xaal.MessageBody) *xaal.MessageBody { value, exist := body["mode"].(string) if exist { // for a unknown reason, we are unable to set the color_mode // z2m doesn't give an error, but the color_mode doesn't change // so we set old color or temperature // // dev.Z2MDevice.Set(`{"color_mode": "color_temp"}`) // dev.Z2MDevice.Set(`{"color_mode": "xy"}`) switch value { case "color": hsv := dev.GetAttribute("hsv").Value.([]float64) color := colorful.Hsv(hsv[0], hsv[1], hsv[2]) dev.Z2MDevice.Set(fmt.Sprintf(`{"color": { "hex": "%s" }}`, color.Hex())) case "white": value := dev.GetAttribute("white_temperature").Value.(int) // if booted in color mode, while_temperature is 0 => divide crash if value == 0 { value = 3000 } value = convertMired(value) dev.Z2MDevice.Set(fmt.Sprintf(`{"color_temp": %d}`, value)) } } return nil } // ============================================================================= // NetButtonRemote // ============================================================================= func NewButtonRemote(addr uuid.UUID, zDev *Z2MDevice, exp *Expose) XAALDeviceInterface { dev := &ButtonRemote{XAALDevice{schemas.NewButtonRemote(addr), zDev, exp}} dev.setup() dev.SetMethod("get_buttons", dev.getButtons) return dev } func (dev *ButtonRemote) update(payload map[string]interface{}) { value, exists := payload[dev.Expose.Name].(string) if exists { body := make(xaal.MessageBody) body["action"] = 0 body["button"] = value dev.SendNotification("click", body) } } func (dev *ButtonRemote) getButtons(xaal.MessageBody) *xaal.MessageBody { body := make(xaal.MessageBody) action := dev.Z2MDevice.GetExpose(dev.Expose.Name) if action != nil { body["buttons"] = action.Values } return &body } // ============================================================================= // Motion // ============================================================================= func NewMotion(addr uuid.UUID, zDev *Z2MDevice, exp *Expose) XAALDeviceInterface { dev := &Motion{XAALDevice{schemas.NewMotion(addr), zDev, exp}} dev.setup() return dev } func (dev *Motion) update(payload map[string]interface{}) { value, exists := payload[dev.Expose.Name].(bool) if exists { dev.GetAttribute("presence").SetValue(value) } } // ============================================================================= // LuxMeter // ============================================================================= func NewLuxMeter(addr uuid.UUID, zDev *Z2MDevice, exp *Expose) XAALDeviceInterface { dev := &LuxMeter{XAALDevice{schemas.NewLuxmeter(addr), zDev, exp}} dev.GetAttribute("illuminance").Value = 0 // override dev.setup() return dev } func (dev *LuxMeter) update(payload map[string]interface{}) { slog.Info("update", "payload", payload) lux, err := convertToInt(payload[dev.Expose.Name]) if err == nil { dev.GetAttribute("illuminance").SetValue(lux) } } // ============================================================================= // Debug Device // ============================================================================= func NewDebugDevice(addr uuid.UUID, zDev *Z2MDevice, exp *Expose) XAALDeviceInterface { dev := &DebugDevice{XAALDevice{schemas.NewBasic(addr), zDev, exp}} dev.setup() return dev } func (dev *DebugDevice) update(payload map[string]interface{}) { slog.Info("Debug Device update", "payload", payload) jsonDump(payload) }