MQTT Client

MQTT library for a JavaScript Plugin

Remarks

The example below is for a client connection to a broker that conforms to the Homie MQTT specification.
https://homieiot.github.io/

The example also demonstrates how raw data values can be mapped to Capabilities.

Example

plugin.OnChangeRequest = onChangeRequest;
plugin.OnConnect = onConnect;
plugin.OnDisconnect = onDisconnect;
plugin.OnPoll = onPoll;
plugin.OnSynchronizeDevices = onSynchronizeDevices;
plugin.PollingInterval = 500;
plugin.Settings = { "URL": "", "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["URL"] + ":" + 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;
}

Methods

connect

Connects to the broker specified by the given url and options.

connect([url], [options])

  • url : The URL can be on the following protocols: 'mqtt', 'mqtts', 'tcp', 'tls', 'ws', 'wss'.
  • options
    • host: The host name or IP address of broker. Not needed when a URL is supplied.
    • port: The port of broker. Not needed when a URL is supplied.
    • keepalive: 60 seconds, set to 0 to disable
    • clientId: Defaults to a randomly generated UUID
    • protocolVersion: 4
    • clean: true, set to false to receive QoS 1 and 2 messages while offline
    • username: the username required by your broker, if any
    • password: the password required by your broker, if any
    • sessionExpiryInterval: representing the Session Expiry Interval in seconds number,
    • receiveMaximum: representing the Receive Maximum value number,
    • maximumPacketSize: representing the Maximum Packet Size the Client is willing to accept number,
    • topicAliasMaximum: representing the Topic Alias Maximum value indicates the highest value that the Client will accept as a Topic Alias sent by the Server number,
    • requestResponseInformation: The Client uses this value to request the Server to return Response Information in the CONNACK boolean,
    • requestProblemInformation: The Client uses this value to indicate whether the Reason String or User Properties are sent in the case of failures boolean,
    • authenticationMethod: the name of the authentication method used for extended authentication string,
    • authenticationData: Binary Data containing authentication data binary
    • rejectUnauthorized: Set to false to ignore certificate errors. Defaults to true.

disconnect

Close connection to the MQTT server.

disconnect([options])

  • options
    • reasonString : The reason for closing the connection.
    • timeout : Time, in milliseconds, to wait before timing out. The default is 0 which waits indefinitely.

publish

Publishes a message to a topic.

publish(topic, message, [options])

  • topic : is the topic to publish to, String.
  • message : is the message to publish, Buffer or String.
  • options
    • qos : QoS level, Number, default 0.
    • retain : retain flag, Boolean, default false.
    • dup : mark as duplicate flag, Boolean, default false.
    • payloadFormatIndicator : Payload is UTF-8 Encoded Character Data or not boolean.
    • messageExpiryInterval : the lifetime of the Application Message in seconds number.
    • topicAlias : value that is used to identify the Topic instead of using the Topic Name number.
    • responseTopic : String which is used as the Topic Name for a response message string.
    • correlationData : used by the sender of the Request Message to identify which request the Response Message is for when it is received binary.
    • userProperties : The User Property is allowed to appear multiple times to represent multiple name, value pairs object.
    • subscriptionIdentifier : representing the identifier of the subscription number.
    • contentType : String describing the content of the Application Message string.
    • timeout : Time, in milliseconds, to wait before timing out. The default is 0 which waits indefinitely.

subscribe

Subscribe to a single topic or an array of topics.

subscribe(topics, message, [options])

  • topic : is a String topic to subscribe to or an Array of topics to subscribe to. MQTT topic wildcard characters are supported (+ - for single level and # - for multi level)
  • options is the options to subscribe with, including:
    • qos : QoS subscription level, default 0
    • nl : No Local MQTT 5.0 flag (If the value is true, Application Messages MUST NOT be forwarded to a connection with a ClientID equal to the ClientID of the publishing connection)
    • rap : Retain as Published MQTT 5.0 flag (If true, Application Messages forwarded using this subscription keep the RETAIN flag they were published with. If false, Application Messages forwarded using this subscription have the RETAIN flag set to 0.)
    • rh : Retain Handling MQTT 5.0 (This option specifies whether retained messages are sent when the subscription is established.)
    • subscriptionIdentifier : representing the identifier of the subscription number,
    • userProperties : The User Property is allowed to appear multiple times to represent multiple name, value pairs object
    • timeout : Time, in milliseconds, to wait before timing out. The default is 0 which waits indefinitely.

unsubscribe

Unsubscribe from a topic or topics.

subscribe(topics, [options])

  • topic is a String topic or an array of topics to unsubscribe from
  • options:
    • userProperties: The User Property is allowed to appear multiple times to represent multiple name, value pairs object
    • timeout : Time, in milliseconds, to wait before timing out. The default is 0 which waits indefinitely.

readMessage

Reads message from the message queue.

readMessage([options])

  • options:
    • timeout : Time, in milliseconds, to wait before timing out. The default is 0 which waits indefinitely.

Returns & removes the oldest message in the queue. If the queue is empty, it'll wait/block until 1 is received.