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;
}

I’ve been struggling trying to get this plugin to work.
When i look at the logs in the broker, I see authentication is passing and it’s publishing some packages.
In home remote you just get an error “The operation has timed out”
any idea what’s going wrong?

I’ve tested the connection with MQTT snooper and there it looks ok from what I can tell…

What does the log show?

Do you ever see the “connected” message that is printed in the logs?

In case you don’t know where the window is, it’s in the bottom left corner (see screenshot)

It only shows “connected”. But despite that, I can’t save due to the error message

It’s probably the code inside onSynchronizeDevices that is causing it. That will wait for a message of the topic “$SYS/broker/version”. If it doesn’t receive that it will timeout. There’s also a timeout of 3000 ms on the readMessage call itself. You can try increasing that too but I think it’s most likely related to the version message. I suggest adding some console.log writes to help troubleshoot. Or if you want to enable remote access on your server, I can look at it this weekend if you’d like.

I’ve increased the time out to 50000, but that didn’t help.
It timed out anyway.
Remote support would really be appreciated,
My time zone is Central Europe, so we’ll have to take that in concideration. When would suite you and which method do you prefer?

Yeah, it’s probably something else that’s holding it up. I didn’t think that was going to be the issue.

Whenever, I should have some time later today, or definitely tomorrow. Just send me the connection info so I can take a look. I’ll need the same parameters you are providing on that plugin setup dialog: Host, Port, etc. You can email it to support@thehomeremote.com or send me a private message on here.