Homie MQTT Plugin

Attached is an example plugin that will connect to a Homie compliant MQTT broker. More info about the Homie convention can be found on their website.
https://homieiot.github.io/

To install the plugin, use the Plugin Import utility in the Designer. The import process will ask you for the Host, Port, Username, & Password of your MQTT broker.

This plugin was specifically written to work with Kevin’s Hubitat MQTT app @xAPPO

Since this plugin conforms to the Homie spec, it should work with other controllers as well; perhaps with only a few minor modifications. The places that will likely need to be modified are related to device properties. For example, switches in Kevin’s app use properties named onoff to represent the current state. If your controller labels these something else, like for example power, you would need to replace all instances of onoff in the plugin with power. Athom Homey controllers happen to use onoff for their switches as well, so this plugin should conceivably connect to the Homey MQTT Hub app as-is without any modifications.

Here is the plugin code:
homie.plugin (12.6 KB)

plugin.Name = "Homie";
plugin.OnChangeRequest = onChangeRequest;
plugin.OnConnect = onConnect;
plugin.OnDisconnect = onDisconnect;
plugin.OnPoll = onPoll;
plugin.OnSynchronizeDevices = onSynchronizeDevices;
plugin.PollingInterval = 500;
plugin.DefaultSettings = { "Host": "", "Port": "", "Username": "", "Password": "" };

var mqtt = new MQTTClient();
var subscribed = false;

function onChangeRequest(device, attribute, value) {
    var deviceIdParts = device.Id.split(":");
    var homieDeviceId = deviceIdParts[0];
    var homieNodeId = deviceIdParts[1];
    switch (attribute) {
        case "Color":
            //HomeRemote's Color Hue ranges from 0-100%
            //we need to scale up to 0-360 for Homie.          
            var hue = Math.round(value.Hue * (360 / 100));
            var sat = value.Saturation;
            var hsv = hue + "," + sat + "," + 100;
            mqtt.publish("homie/" + homieDeviceId + "/" + homieNodeId + "/color/set", hsv);
            break;
        case "ColorTemperature":
            mqtt.publish("homie/" + homieDeviceId + "/" + homieNodeId + "/color-temperature/set", value);
            break;
        case "CoolingSetpoint":
            mqtt.publish("homie/" + homieDeviceId + "/" + homieNodeId + "/cooling-setpoint/set", value);
            break;
        case "HeatingSetpoint":
            mqtt.publish("homie/" + homieDeviceId + "/" + homieNodeId + "/heating-setpoint/set", value);
            break;
        case "Level":
            mqtt.publish("homie/" + homieDeviceId + "/" + homieNodeId + "/dim/set", value);
            break;
        case "Lock":
            mqtt.publish("homie/" + homieDeviceId + "/" + homieNodeId + "/lock/set", ((value == "Locked") ? "true" : "false"));
            break;
        case "Switch":
            mqtt.publish("homie/" + homieDeviceId + "/" + homieNodeId + "/onoff/set", ((value == "On") ? "true" : "false"));
            break;
        case "ThermostatFanMode":
            mqtt.publish("homie/" + homieDeviceId + "/" + homieNodeId + "/fanmode/set", convertToCamelCase(value));
            break;
        case "ThermostatMode":
            mqtt.publish("homie/" + homieDeviceId + "/" + homieNodeId + "/mode/set", convertToCamelCase(value));
            break;
        case "Variable":
            mqtt.publish("homie/" + homieDeviceId + "/" + homieNodeId + "/variable/set", value);
            break;
        default:
            break;
    }
}

function onConnect() {
    mqtt.connect("mqtt://" + plugin.Settings["Username"] + ":" + plugin.Settings["Password"] + "@" + plugin.Settings["Host"] + ":" + plugin.Settings["Port"]);
    //This method is shared with both "onPoll" & "onSynchronizeDevices"
    //We need to subscribe to different sets of topics for those functions.
    //That's why we aren't subscribing here.
    //Connecting with default options will clean our subscriptions, so we need to reset our "subscribed" variable.
    subscribed = false;
    console.log("connected");
}

function onDisconnect() {
    mqtt.disconnect();
    console.log("disconnected");
}

function onPoll() {
    //Even though we have an infinite "while" loop below that reads messages, we need to consider the possiblity that a previous "onPoll" call was cancelled.
    //And we don't want to send more subscribe requests than we need.
    //So after we subscribe, set the "subscribed" variable so we know we are confidently subscribed.
    if (!subscribed) {
        var subscribeTopics = [
            "homie/+/+/color",
            "homie/+/+/color-temperature",
            "homie/+/+/cooling-setpoint",
            "homie/+/+/contact",
            "homie/+/+/dim",
            "homie/+/+/fanmode",
            "homie/+/+/heating-setpoint",
            "homie/+/+/lock",
            "homie/+/+/measure-battery",
            "homie/+/+/measure-humidity",
            "homie/+/+/measure-light",
            "homie/+/+/measure-power",
            "homie/+/+/measure-temperature",
            "homie/+/+/mode",
            "homie/+/+/motion",
            "homie/+/+/onoff",
            "homie/+/+/presence-sensor",
            "homie/+/+/state",
            "homie/+/+/variable"
        ];
        mqtt.subscribe(subscribeTopics);
        subscribed = true;
    }
    while (true) {
        var message = mqtt.readMessage();
        var topicParts = message.topic.split("/");
        if (topicParts.length > 3) {
            var homieDeviceId = topicParts[1];
            var homieNodeId = topicParts[2];
            var deviceId = homieDeviceId + ":" + homieNodeId;
            var device = plugin.Devices[deviceId];
            if (device != null) {
                var payload = message.payload.toString();
                var propertyPath = topicParts.slice(3).join("/");
                switch (propertyPath) {
                    case "color":
                        //HomeRemote's color map needs Hue in 0-100% range.
                        //We need to scale down the 0-360 Homie value.
                        var hsv = payload.split(",");
                        var hue = Math.round(hsv[0] * (100 / 360));
                        var sat = hsv[1];
                        device.Color = { Hue: hue, Saturation: sat };
                        break;
                    case "color-temperature":
                        device.ColorTemperature = payload;
                        break;
                    case "cooling-setpoint":
                        device.CoolingSetpoint = payload;
                        break;
                    case "contact":
                        device.Contact = ((payload == "true") ? "Open" : "Closed");
                        break;
                    case "dim":
                        device.Level = payload;
                        break;
                    case "fanmode":
                        device.ThermostatFanMode = convertToPascalCase(payload);
                        break;
                    case "heating-setpoint":
                        device.HeatingSetpoint = payload;
                        break;
                    case "lock":
                        device.Lock = ((payload == "true") ? "Locked" : "Unlocked");
                        break;
                    case "measure-battery":
                        device.Battery = payload;
                        break;
                    case "measure-humidity":
                        device.Humidity = Math.round(payload);
                        break;
                    case "measure-light":
                        device.Illuminance = Math.round(payload);
                        break;
                    case "measure-power":
                        device.Power = Math.round(payload);
                        break;
                    case "measure-temperature":
                        device.Temperature = Math.round(payload);
                        break;
                    case "mode":
                        device.ThermostatMode = convertToPascalCase(payload);
                        break;
                    case "motion":
                        device.Motion = ((payload == "true") ? "Active" : "Inactive");;
                        break;
                    case "onoff":
                        device.Switch = ((payload == "true") ? "On" : "Off");
                        break;
                    case "presence-sensor":
                        device.Presence = ((payload == "true") ? "Present" : "NotPresent");
                        break;
                    case "state":
                        device.ThermostatOperatingState = convertToPascalCase(payload);
                        break;
                    case "variable":
                        device.Variable = payload;
                        break;
                    default:
                        break;
                }
            }
        }
    }
}

function onSynchronizeDevices() {
    //Subscribing to $SYS topic as a way of letting us know we've received all messages for $name & $properties.
    mqtt.subscribe(["homie/+/+/$name", "homie/+/+/$properties", "$SYS/broker/version"]);
    while (true) {
        var message = mqtt.readMessage({ timeout: 3000 });
        if (message.topic == "$SYS/broker/version") {
            //We know this is the last topic.  Let's exit.
            return;
        }
        var payload = message.payload.toString();
        if (payload) {
            var topicParts = message.topic.split("/");
            var homieDeviceId = topicParts[1];
            var homieNodeId = topicParts[2];
            var homieNodeAttr = topicParts[3];
            var deviceId = homieDeviceId + ":" + homieNodeId;
            var device = plugin.Devices[deviceId];
            if (device == null) {
                device = new Device();
                device.Id = deviceId;
                plugin.Devices[deviceId] = device;
            }
            if (homieNodeAttr == "$name") {
                device.DisplayName = payload;
            }
            else if (homieNodeAttr == "$properties") {
                var properties = payload.split(",");
                var capabilities = [];
                var attributes = [];
                properties.forEach(function (propertyName) {
                    switch (propertyName.trim()) {
                        case "color":
                            capabilities.push("ColorControl");
                            break;
                        case "color-temperature":
                            capabilities.push("ColorTemperature");
                            break;
                        case "cooling-setpoint":
                            capabilities.push("ThermostatCoolingSetpoint");
                            break;
                        case "contact":
                            capabilities.push("ContactSensor");
                            break;
                        case "dim":
                            capabilities.push("SwitchLevel");
                            break;
                        case "fanmode":
                            capabilities.push("ThermostatFanMode");
                            break;
                        case "heating-setpoint":
                            capabilities.push("ThermostatHeatingSetpoint");
                            break;
                        case "lock":
                            capabilities.push("Lock");
                            break;
                        case "measure-battery":
                            capabilities.push("Battery");
                            break;
                        case "measure-humidity":
                            capabilities.push("RelativeHumidityMeasurement");
                            break;
                        case "measure-light":
                            capabilities.push("IlluminanceMeasurement");
                            break;
                        case "measure-power":
                            capabilities.push("PowerMeter");
                            break;
                        case "measure-temperature":
                            capabilities.push("TemperatureMeasurement");
                            break;
                        case "mode":
                            capabilities.push("ThermostatMode");
                            break;
                        case "motion":
                            capabilities.push("MotionSensor");
                            break;
                        case "presence-sensor":
                            capabilities.push("PresenceSensor");
                            break;
                        case "onoff":
                            capabilities.push("Switch");
                            break;
                        case "state":
                            capabilities.push("ThermostatOperatingState");
                            break;
                        case "variable":
                            attributes.push("Variable");
                            break;
                        default:
                            break;
                    }
                });
                device.Capabilities = capabilities;
                device.Attributes = attributes;
            }
        }
    }
}

function convertToCamelCase(value) {
    if (value) {
        return value.charAt(0).toLowerCase() + value.slice(1);
    }
    return value;
}

function convertToPascalCase(value) {
    if (value) {
        return value.charAt(0).toUpperCase() + value.slice(1);
    }
    return value;
}