Skip to content
Snippets Groups Projects
xaal.go 13.66 KiB
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 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{}) {
	lux, err := convertToInt(payload[dev.Expose.Name])
	if err == nil {
		dev.GetAttribute("illuminance").SetValue(lux)
	}
}