Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4ccd19c
Adding LWT/Birth/Death messages
hardillb Dec 2, 2025
49fe669
fix lint
hardillb Dec 2, 2025
0ba75e1
all the lint
hardillb Dec 2, 2025
e0da296
Frontend working (needs styling)
hardillb Dec 4, 2025
aa39184
fix lint
hardillb Dec 4, 2025
ba72786
Basically working (needs testing)
hardillb Dec 5, 2025
72479bc
Fix tests
hardillb Dec 5, 2025
62c1be4
Working...
hardillb Dec 5, 2025
cdda04c
Add missing file
hardillb Dec 11, 2025
4b973c3
Update test/unit/ff-mqtt_spec.js
hardillb Dec 15, 2025
d9198c6
Fix linking
hardillb Dec 19, 2025
a3b5e61
Merge branch 'lwt-messages' of github.com:FlowFuse/nr-mqtt-nodes into…
hardillb Dec 19, 2025
6b943b2
style the LWT button
hardillb Dec 29, 2025
ed724b0
Do both in and out nodes
hardillb Dec 29, 2025
29c4ab7
Deduplicate LWT config nodes
hardillb Jan 6, 2026
aa356ab
remove onadd handlers
Steve-Mcl Jan 6, 2026
2d3f2bf
linting
Steve-Mcl Jan 6, 2026
2259c45
fix debounce
Steve-Mcl Jan 6, 2026
b99d519
Refactor singleton LWT configuration handling:
Steve-Mcl Jan 6, 2026
2f01691
fix remaining UI issues with node status/config count not correctly r…
Steve-Mcl Jan 7, 2026
9ef343d
fix thrown exception when error state is cleared
Steve-Mcl Jan 7, 2026
a078aec
fix: add close handler for static broker node to ensure proper discon…
Steve-Mcl Jan 7, 2026
8a58bdb
fix: update i18n keys for QoS and retain labels
Steve-Mcl Jan 7, 2026
9f612d0
fix: local i18n translations
Steve-Mcl Jan 7, 2026
5322132
fix: implement LWT configuration sections for MQTT broker node
Steve-Mcl Jan 7, 2026
691a8bf
fix: add LWT message saving functionality in MQTT configuration
Steve-Mcl Jan 7, 2026
f2aa171
Merge branch 'main' into lwt-messages
Steve-Mcl Jan 7, 2026
ae16a75
lint
Steve-Mcl Jan 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
314 changes: 312 additions & 2 deletions nodes/ff-mqtt.html
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@
<option value="base64" data-i18n="ff-mqtt.output.base64"></option>
</select>
</div>
<div class="form-row">
<label><i class="fa fa-cog"></i> Config</label>
<button id="node-input-lwt-placeholder" class="red-ui-button">LWT/Birth/Death Messages</button>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
Expand Down Expand Up @@ -220,13 +224,180 @@
<input id="node-input-expiry" style="width: calc(100% - 166px);" class="mqtt-form-row-col1" >
</div>

<div class="form-row">
<label><i class="fa fa-cog"></i> Config</label>
<button id="node-input-lwt-placeholder" class="red-ui-button">LWT/Birth/Death Messages</button>
</div>

<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<div class="form-tips"><span data-i18n="ff-mqtt.tip"></span></div>
</script>

<script type="text/html" data-template-name="ff-mqtt-conf">
<div class="red-ui-palette-header">
<i class="fa fa-angle-down"></i><span data-i18n="ff-mqtt.sections-label.birth-message"></span>
</div>
<div class="section-content" style="padding:10px 0 0 10px">
<div class="form-row">
<label style="width: 100px !important;" for="node-config-input-birthTopic"><i class="fa fa-tasks"></i> <span
data-i18n="common.label.topic"></span></label>
<input style="width: calc(100% - 300px) !important" type="text" id="node-config-input-birthTopic"
data-i18n="[placeholder]ff-mqtt.placeholder.birth-topic">
<label style="margin-left: 10px; width: 90px !important;" for="node-config-input-birthRetain"><i
class="fa fa-history"></i> <span data-i18n="ff-mqtt.label.retain"></span></label>
<select id="node-config-input-birthRetain" style="width:75px !important">
<option value="false" data-i18n="ff-mqtt.false"></option>
<option value="true" data-i18n="ff-mqtt.true"></option>
</select>
</div>
<div class="form-row">
<label style="width: 100px !important;" for="node-config-input-birthPayload"><i class="fa fa-envelope"></i>
<span data-i18n="common.label.payload"></span></label>
<input style="width: calc(100% - 300px) !important" type="text" id="node-config-input-birthPayload"
style="width:300px" data-i18n="[placeholder]common.label.payload">
<label style="margin-left: 10px; width: 90px !important;" for="node-config-input-birthQos"><i
class="fa fa-empire"></i> <span data-i18n="ff-mqtt.label.qos"></span></label>
<select id="node-config-input-birthQos" style="width:75px !important">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
</select>
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-birth-contentType" data-i18n="ff-mqtt.label.contentType"></label>
<input type="text" style="width:calc(100% - 200px);" id="node-config-input-birth-contentType">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-birth-props" data-i18n="ff-mqtt.label.userProperties"></label>
<input type="text" style="width:calc(100% - 200px);" id="node-config-input-birth-props">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-birth-respTopic"><span data-i18n="ff-mqtt.label.responseTopic"></span></label>
<input type="text" id="node-config-input-birth-respTopic" style="width: calc(100% - 200px);">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-birth-correl"><span data-i18n="ff-mqtt.label.correlationData"></span></label>
<input type="text" id="node-config-input-birth-correl" style="width: calc(100% - 200px);">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-birth-expiry"><span data-i18n="ff-mqtt.label.expiry"></span></label>
<input id="node-config-input-birth-expiry" style="width: calc(100% - 200px);">
</div>
</div>
</div>
<div id="mqtt-broker-section-close">
<div class="red-ui-palette-header">
<i class="fa fa-angle-down"></i><span data-i18n="ff-mqtt.sections-label.close-message"></span>
</div>
<div class="section-content" style="padding:10px 0 0 10px">
<div class="form-row">
<label style="width: 100px !important;" for="node-config-input-closeTopic"><i class="fa fa-tasks"></i> <span
data-i18n="common.label.topic"></span></label>
<input style="width: calc(100% - 300px) !important" type="text" id="node-config-input-closeTopic"
style="width:300px" data-i18n="[placeholder]ff-mqtt.placeholder.close-topic">
<label style="margin-left: 10px; width: 90px !important;" for="node-config-input-closeRetain"><i
class="fa fa-history"></i> <span data-i18n="ff-mqtt.label.retain"></span></label>
<select id="node-config-input-closeRetain" style="width:75px !important">
<option value="false" data-i18n="ff-mqtt.false"></option>
<option value="true" data-i18n="ff-mqtt.true"></option>
</select>
</div>
<div class="form-row">
<label style="width: 100px !important;" for="node-config-input-closePayload"><i class="fa fa-envelope"></i>
<span data-i18n="common.label.payload"></span></label>
<input style="width: calc(100% - 300px) !important" type="text" id="node-config-input-closePayload"
style="width:300px" data-i18n="[placeholder]common.label.payload">
<label style="margin-left: 10px; width: 90px !important;" for="node-config-input-closeQos"><i
class="fa fa-empire"></i> <span data-i18n="mqtt.label.qos"></span></label>
<select id="node-config-input-closeQos" style="width:75px !important">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
</select>
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-close-contentType" data-i18n="ff-mqtt.label.contentType"></label>
<input type="text" style="width:calc(100% - 200px);" id="node-config-input-close-contentType">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-close-props" data-i18n="ff-mqtt.label.userProperties"></label>
<input type="text" style="width:calc(100% - 200px);" id="node-config-input-close-props">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-close-respTopic"><span data-i18n="ff-mqtt.label.responseTopic"></span></label>
<input type="text" id="node-config-input-close-respTopic" style="width: calc(100% - 200px);">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-close-correl"><span data-i18n="ff-mqtt.label.correlationData"></span></label>
<input type="text" id="node-config-input-close-correl" style="width: calc(100% - 200px);">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-close-expiry"><span data-i18n="ff-mqtt.label.expiry"></span></label>
<input id="node-config-input-close-expiry" style="width: calc(100% - 200px);">
</div>
</div>
</div>
<div id="mqtt-broker-section-will">
<div class="red-ui-palette-header">
<i class="fa fa-angle-down"></i><span data-i18n="ff-mqtt.sections-label.will-message"></span>
</div>
<div class="section-content" style="padding:10px 0 0 10px">
<div class="form-row">
<label style="width: 100px !important;" for="node-config-input-willTopic"><i class="fa fa-tasks"></i> <span
data-i18n="common.label.topic"></span></label>
<input style="width: calc(100% - 300px) !important" type="text" id="node-config-input-willTopic"
style="width:300px" data-i18n="[placeholder]ff-mqtt.placeholder.will-topic">
<label style="margin-left: 10px; width: 90px !important;" for="node-config-input-willRetain"><i
class="fa fa-history"></i> <span data-i18n="mqtt.label.retain"></span></label>
<select id="node-config-input-willRetain" style="width:75px !important">
<option value="false" data-i18n="ff-mqtt.false"></option>
<option value="true" data-i18n="ff-mqtt.true"></option>
</select>
</div>
<div class="form-row">
<label style="width: 100px !important;" for="node-config-input-willPayload"><i class="fa fa-envelope"></i>
<span data-i18n="common.label.payload"></span></label>
<input style="width: calc(100% - 300px) !important" type="text" id="node-config-input-willPayload"
style="width:300px" data-i18n="[placeholder]common.label.payload">
<label style="margin-left: 10px; width: 90px !important;" for="node-config-input-willQos"><i
class="fa fa-empire"></i> <span data-i18n="ff-mqtt.label.qos"></span></label>
<select id="node-config-input-willQos" style="width:75px !important">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
</select>
</div>
<div class="form-row mqtt5">
<label><span data-i18n="ff-mqtt.label.delay"></span></label>
<input type="number" min="0" id="node-config-input-will-delay" style="width: 100px">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-will-contentType" data-i18n="ff-mqtt.label.contentType"></label>
<input type="text" style="width:calc(100% - 200px);" id="node-config-input-will-contentType">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-will-props" data-i18n="ff-mqtt.label.userProperties"></label>
<input type="text" style="width:calc(100% - 200px);" id="node-config-input-will-props">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-will-respTopic"><span data-i18n="ff-mqtt.label.responseTopic"></span></label>
<input type="text" id="node-config-input-will-respTopic" style="width: calc(100% - 200px);">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-will-correl"><span data-i18n="ff-mqtt.label.correlationData"></span></label>
<input type="text" id="node-config-input-will-correl" style="width: calc(100% - 200px);">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-will-expiry"><span data-i18n="ff-mqtt.label.expiry"></span></label>
<input id="node-config-input-will-expiry" style="width: calc(100% - 200px);">
</div>
</div>
</div>
</script>

<script type="text/javascript">
/* global RED, $ */
(function () {
Expand Down Expand Up @@ -289,6 +460,15 @@
}
})
}
function mapDefaults (defaults) {
const values = {}
for (const key in defaults) {
if (Object.prototype.hasOwnProperty.call(defaults, key)) {
values[key] = defaults[key].value
}
}
return values
}
RED.nodes.registerType('ff-mqtt-in', {
category: 'FlowFuse',
defaults: {
Expand All @@ -312,7 +492,8 @@
nl: { value: false },
rap: { value: true },
rh: { value: 0 },
inputs: { value: 0 }
inputs: { value: 0 },
lwt: { type: 'ff-mqtt-conf' }
},
color: '#d8bfd8',
inputs: 0,
Expand Down Expand Up @@ -357,6 +538,10 @@
$('div.form-row.form-row-mqtt-static').toggleClass('form-row-mqtt-static-disabled', !!dynamic)
}

$('#node-input-lwt-placeholder').on('click', function () {
RED.editor.editConfig('', 'ff-mqtt-conf', node.lwt)
})

// $('#node-input-broker').on('change', function (d) {
// updateVisibility()
// })
Expand Down Expand Up @@ -385,6 +570,31 @@
if ($('#node-input-topicType').val() === 'dynamic') {
$('#node-input-topic').val('')
}
},
onadd: function () {
let found
RED.nodes.eachConfig(function (n) {
if (n.type === 'ff-mqtt-conf') {
found = n.id
}
})
if (found) {
this.lwt = found
} else {
const base = RED.nodes.getType('ff-mqtt-conf')
const LWTnode = {
_def: base,
id: RED.nodes.id(),
type: 'ff-mqtt-conf',
...mapDefaults(base.defaults),
users: []
}
LWTnode.dirty = true
LWTnode.changed = true
RED.nodes.add(LWTnode)
RED.editor.validateNode(LWTnode)
this.lwt = LWTnode.id
}
}
})

Expand All @@ -399,7 +609,8 @@
contentType: { value: '' },
userProps: { value: '' },
correl: { value: '' },
expiry: { value: '' }
expiry: { value: '' },
lwt: { type: 'ff-mqtt-conf' }
},
color: '#d8bfd8',
inputs: 1,
Expand Down Expand Up @@ -439,6 +650,10 @@
}
}

$('#node-input-lwt-placeholder').on('click', function () {
RED.editor.editConfig('', 'ff-mqtt-conf', that.lwt)
})

// $('#node-input-broker').on('change', function (d) {
showHideDynamicFields()
// })
Expand Down Expand Up @@ -481,8 +696,103 @@
},
labelStyle: function () {
return this.name ? 'node_label_italic' : ''
},
onadd: function () {
let found
RED.nodes.eachConfig(function (n) {
if (n.type === 'ff-mqtt-conf') {
found = n.id
}
})
if (found) {
this.lwt = found
} else {
const base = RED.nodes.getType('ff-mqtt-conf')
const LWTnode = {
_def: base,
id: RED.nodes.id(),
type: 'ff-mqtt-conf',
...mapDefaults(base.defaults),
users: []
}
LWTnode.dirty = true
LWTnode.changed = true
RED.nodes.add(LWTnode)
RED.editor.validateNode(LWTnode)
this.lwt = LWTnode.id
}
}
})

RED.nodes.registerType('ff-mqtt-conf', {
category: 'config',
defaults: {
birthTopic: { value: '', validate: validateMQTTPublishTopic },
birthQos: { value: '0' },
birthRetain: { value: 'false' },
birthPayload: { value: '' },
birthMsg: { value: {} },
closeTopic: { value: '', validate: validateMQTTPublishTopic },
closeQos: { value: '0' },
closeRetain: { value: 'false' },
closePayload: { value: '' },
closeMsg: { value: {} },
willTopic: { value: '', validate: validateMQTTPublishTopic },
willQos: { value: '0' },
willRetain: { value: 'false' },
willPayload: { value: '' },
willMsg: { value: {} }
},
label: function () {
return 'Team Broker LWT'
}
})

function debounce (func, wait, immediate) {
let timeout
return function () {
const context = this; const args = arguments
const later = function () {
timeout = null
if (!immediate) func.apply(context, args)
}
const callNow = immediate && !timeout
clearTimeout(timeout)
timeout = setTimeout(later, wait)
if (callNow) func.apply(context, args)
}
}

function singletonLWTConfig (node) {
if (node.type === 'ff-mqtt-conf') {
const ffConf = []
RED.nodes.eachConfig((n) => {
if (n.type === 'ff-mqtt-conf') {
ffConf.push(n)
}
})
// Remove all but first
while (ffConf.length > 1) {
const n = ffConf.pop()
RED.nodes.remove(n.id)
RED.nodes.dirty(true)
}
if (ffConf.length > 0) {
const lwt = ffConf[0].id
RED.nodes.eachNode((n) => {
if (n.type === 'ff-mqtt-in' || n.type === 'ff-mqtt-out') {
if (n.lwt !== lwt) {
n.lwt = lwt
}
}
})
}
}
}

RED.events.on('nodes:add', function (node) {
debounce(singletonLWTConfig, 25)
})
})()
</script>

Loading