Milesight LoraWAN Router

Got my hands on a LoraWAN router and a thermostat.

The Router has an integrated network server, so you don't need an externel Application server.

You can activate an outdated Node-RED-Server (v1.2.9) inside the Router for building your logic and connect to an mqtt server for talking to the thermostat. After some fiddeling with the bits and bytes, i was able to decode and encode data from/to the thermostat.

De/Encoder copied from here:

I will make some more range testing, but all in all the connection seems quite reliable.

My Prototype flow:

[{"id":"c4cffc88.53d56","type":"subflow","name":"encode & send","info":"","category":"","in":[{"x":60,"y":80,"wires":[{"id":"7463a96c.43218"}]}],"out":[],"env":[],"color":"#DDAA99","status":{"x":920,"y":140,"wires":[{"id":"aa869a8d.895e38","port":0}]}},{"id":"7463a96c.43218","type":"function","z":"c4cffc88.53d56","name":"Payload Encoder","func":"/**\n * Payload Encoder\n *\n * Copyright 2024 Milesight IoT\n *\n * @product WT101\n */\n// Chirpstack v4\nfunction encodeDownlink(input) {\n    var encoded = milesightDeviceEncode(input.data);\n    return encoded;\n}\n\n// Chirpstack v3\nfunction Encode(fPort, obj) {\n    var encoded = milesightDeviceEncode(obj);\n    return encoded;\n}\n\n// The Things Network\nfunction Encoder(obj, port) {\n    return milesightDeviceEncode(obj);\n}\n\nfunction milesightDeviceEncode(payload) {\n    var encoded = [];\n\n    if (\"reboot\" in payload) {\n        encoded = encoded.concat(reboot(payload.reboot));\n    }\n    if (\"report_status\" in payload) {\n        encoded = encoded.concat(reportStatus(payload.report_status));\n    }\n    if (\"sync_time\" in payload) {\n        encoded = encoded.concat(syncTime(payload.sync_time));\n    }\n    if (\"report_interval\" in payload) {\n        encoded = encoded.concat(setReportInterval(payload.report_interval));\n    }\n    if (\"timezone\" in payload) {\n        encoded = encoded.concat(setTimeZone(payload.timezone));\n    }\n    if (\"time_sync_enable\" in payload) {\n        encoded = encoded.concat(setTimeSyncEnable(payload.time_sync_enable));\n    }\n    if (\"temperature_calibration\" in payload) {\n        encoded = encoded.concat(setTemperatureCalibration(payload.temperature_calibration.enable, payload.temperature_calibration.temperature));\n    }\n    if (\"temperature_control\" in payload && \"enable\" in payload.temperature_control) {\n        encoded = encoded.concat(setTemperatureControl(payload.temperature_control.enable));\n    }\n    if (\"temperature_control\" in payload && \"mode\" in payload.temperature_control) {\n        encoded = encoded.concat(setTemperatureControlMode(payload.temperature_control.mode));\n    }\n    if (\"temperature_target\" in payload) {\n        encoded = encoded.concat(setTemperatureTarget(payload.temperature_target, payload.temperature_error));\n    }\n    if (\"open_window_detection\" in payload) {\n        encoded = encoded.concat(setOpenWindowDetection(payload.open_window_detection.enable, payload.open_window_detection.temperature_threshold, payload.open_window_detection.time));\n    }\n    if (\"restore_open_window_detection\" in payload) {\n        encoded = encoded.concat(restoreOpenWindowDetection(payload.restore_open_window_detection));\n    }\n    if (\"valve_opening\" in payload) {\n        encoded = encoded.concat(setValveOpening(payload.valve_opening));\n    }\n    if (\"valve_calibration\" in payload) {\n        encoded = encoded.concat(setValveCalibration(payload.valve_calibration));\n    }\n    if (\"valve_control_algorithm\" in payload) {\n        encoded = encoded.concat(setValveControlAlgorithm(payload.valve_control_algorithm));\n    }\n    if (\"freeze_protection_config\" in payload) {\n        encoded = encoded.concat(setFreezeProtection(payload.freeze_protection_config.enable, payload.freeze_protection_config.temperature));\n    }\n    if (\"child_lock_config\" in payload) {\n        encoded = encoded.concat(setChildLockEnable(payload.child_lock_config.enable));\n    }\n    return encoded;\n}\n\n/**\n * device reboot\n * @param {number} reboot\n * @example { \"reboot\": 1 }\n */\nfunction reboot(reboot) {\n    var reboot_values = [0, 1];\n    if (reboot_values.indexOf(reboot) == -1) {\n        throw new Error(\"reboot must be one of \" + reboot_values.join(\", \"));\n    }\n\n    if (reboot === 0) {\n        return [];\n    }\n    return [0xff, 0x10, 0xff];\n}\n\n/**\n * sync time\n * @param {number} sync_time\n * @example { \"sync_time\": 1 }\n */\nfunction syncTime(sync_time) {\n    var sync_time_values = [0, 1];\n    if (sync_time_values.indexOf(sync_time) === -1) {\n        throw new Error(\"sync_time must be one of \" + sync_time_values.join(\", \"));\n    }\n\n    if (sync_time === 0) {\n        return [];\n    }\n    return [0xff, 0x4a, 0xff];\n}\n\n/**\n * report status\n * @param {number} report_status\n * @example { \"report_status\": 1 }\n */\nfunction reportStatus(report_status) {\n    var report_status_values = [0, 1];\n    if (report_status_values.indexOf(report_status) == -1) {\n        throw new Error(\"report_status must be one of \" + report_status_values.join(\", \"));\n    }\n\n    if (report_status === 0) {\n        return [];\n    }\n    return [0xff, 0x28, 0xff];\n}\n\n/**\n * time zone configuration\n * @param {number} timezone range: [-12, 12]\n * @example { \"timezone\": -4 }\n * @example { \"timezone\": 8 }\n */\nfunction setTimeZone(timezone) {\n    if (typeof timezone !== \"number\") {\n        throw new Error(\"timezone must be a number\");\n    }\n    if (timezone < -12 || timezone > 12) {\n        throw new Error(\"timezone must be between -12 and 12\");\n    }\n\n    var buffer = new Buffer(4);\n    buffer.writeUInt8(0xff);\n    buffer.writeUInt8(0x17);\n    buffer.writeInt16LE(timezone * 10);\n    return buffer.toBytes();\n}\n\n/**\n * report interval configuration\n * @param {number} report_interval uint: minute\n * @example { \"report_interval\": 10 }\n */\nfunction setReportInterval(report_interval) {\n    if (typeof report_interval !== \"number\") {\n        throw new Error(\"report_interval must be a number\");\n    }\n\n    var buffer = new Buffer(5);\n    buffer.writeUInt8(0xff);\n    buffer.writeUInt8(0x8e);\n    buffer.writeUInt8(0x00);\n    buffer.writeUInt16LE(report_interval);\n    return buffer.toBytes();\n}\n\n/**\n * time sync configuration\n * @param {number} time_sync_enable\n * @example { \"time_sync_enable\": 0 }\n */\nfunction setTimeSyncEnable(time_sync_enable) {\n    var time_sync_enable_values = [0, 1];\n    if (time_sync_enable_values.indexOf(time_sync_enable) == -1) {\n        throw new Error(\"time_sync_enable must be one of \" + time_sync_enable_values.join(\", \"));\n    }\n\n    var buffer = new Buffer(3);\n    buffer.writeUInt8(0xff);\n    buffer.writeUInt8(0x3b);\n    buffer.writeUInt8(time_sync_enable);\n    return buffer.toBytes();\n}\n\n/**\n * temperature calibration configuration\n * @param {number} enable\n * @param {number} temperature uint: Celsius\n * @example { \"temperature_calibration\": { \"enable\": 1, \"temperature\": 5 }}\n * @example { \"temperature_calibration\": { \"enable\": 1, \"temperature\": -5 }}\n * @example { \"temperature_calibration\": { \"enable\": 0 } }\n */\nfunction setTemperatureCalibration(enable, temperature) {\n    var temperature_calibration_enable_values = [0, 1];\n    if (temperature_calibration_enable_values.indexOf(enable) == -1) {\n        throw new Error(\"temperature_calibration.enable must be one of \" + temperature_calibration_enable_values.join(\", \"));\n    }\n    if (enable && typeof temperature !== \"number\") {\n        throw new Error(\"temperature_calibration.temperature must be a number\");\n    }\n\n    var buffer = new Buffer(5);\n    buffer.writeUInt8(0xff);\n    buffer.writeUInt8(0xab);\n    buffer.writeUInt8(enable);\n    buffer.writeInt16LE(temperature * 10);\n    return buffer.toBytes();\n}\n\n/**\n * temperature control enable configuration\n * @param {number} enable\n * @example { \"temperature_control\": { \"enable\": 1 } }\n */\nfunction setTemperatureControl(enable) {\n    var temperature_control_enable_values = [0, 1];\n    if (temperature_control_enable_values.indexOf(enable) == -1) {\n        throw new Error(\"temperature_control.enable must be one of \" + temperature_control_enable_values.join(\", \"));\n    }\n\n    var buffer = new Buffer(3);\n    buffer.writeUInt8(0xff);\n    buffer.writeUInt8(0xb3);\n    buffer.writeUInt8(enable);\n    return buffer.toBytes();\n}\n\n/**\n * temperature control mode configuration\n * @param {string} mode, values: (0: auto, 1: manual)\n * @example { \"temperature_control\": { \"mode\": 0 } }\n * @example { \"temperature_control\": { \"mode\": 1 } }\n */\nfunction setTemperatureControlMode(mode) {\n    var temperature_control_mode_values = [0, 1];\n    if (temperature_control_mode_values.indexOf(mode) == -1) {\n        throw new Error(\"temperature_control.mode must be one of \" + temperature_control_mode_values.join(\", \"));\n    }\n\n    var buffer = new Buffer(3);\n    buffer.writeUInt8(0xff);\n    buffer.writeUInt8(0xae);\n    buffer.writeUInt8(mode);\n    return buffer.toBytes();\n}\n\n/**\n * temperature target configuration\n * @param {number} temperature_target uint: Celsius\n * @param {number} temperature_error uint: Celsius\n * @example { \"temperature_target\": 10, \"temperature_error\": 0.1 }\n * @example { \"temperature_target\": 28, \"temperature_error\": 5 }\n */\nfunction setTemperatureTarget(temperature_target, temperature_error) {\n    if (typeof temperature_target !== \"number\") {\n        throw new Error(\"temperature_target must be a number\");\n    }\n    if (typeof temperature_error !== \"number\") {\n        throw new Error(\"temperature_error must be a number\");\n    }\n\n    var buffer = new Buffer(4);\n    buffer.writeUInt8(0xff);\n    buffer.writeUInt8(0xb1);\n    buffer.writeInt8(temperature_target);\n    buffer.writeUInt16LE(temperature_error * 10);\n    return buffer.toBytes();\n}\n\n/**\n * open window detection configuration *\n * @param {number} enable\n * @param {number} temperature_threshold uint: Celsius\n * @param {number} time uint: minute\n * @example { \"open_window_detection\": { \"enable\": 1, \"temperature_threshold\": 2, \"time\": 1 } }\n * @example { \"open_window_detection\": { \"enable\": 1, \"temperature_threshold\": 10, \"time\": 1440 } }\n * @example { \"open_window_detection\": { \"enable\": 0 } }\n */\nfunction setOpenWindowDetection(enable, temperature_threshold, time) {\n    var open_window_detection_enable_values = [0, 1];\n    if (open_window_detection_enable_values.indexOf(enable) == -1) {\n        throw new Error(\"open_window_detection.enable must be one of \" + open_window_detection_enable_values.join(\", \"));\n    }\n    if (enable && typeof temperature_threshold !== \"number\") {\n        throw new Error(\"open_window_detection.temperature_threshold must be a number\");\n    }\n    if (enable && typeof time !== \"number\") {\n        throw new Error(\"open_window_detection.time must be a number\");\n    }\n\n    var buffer = new Buffer(6);\n    buffer.writeUInt8(0xff);\n    buffer.writeUInt8(0xaf);\n    buffer.writeUInt8(enable);\n    buffer.writeInt8(temperature_threshold * 10);\n    buffer.writeUInt16LE(time);\n    return buffer.toBytes();\n}\n\n/**\n * restore open window detection status\n * @param {number} restore_open_window_detection\n * @example { \"restore_open_window_detection\": 1 }\n */\nfunction restoreOpenWindowDetection(restore_open_window_detection) {\n    var restore_open_window_detection_values = [0, 1];\n    if (restore_open_window_detection_values.indexOf(restore_open_window_detection) == -1) {\n        throw new Error(\"restore_open_window_detection must be one of \" + restore_open_window_detection_values.join(\", \"));\n    }\n\n    if (restore_open_window_detection === 0) {\n        return [];\n    }\n    return [0xff, 0x57, 0xff];\n}\n\n/**\n * valve opening configuration\n * @param {number} valve_opening uint: percentage, range: [0, 100]\n * @example { \"valve_opening\": 50 }\n */\nfunction setValveOpening(valve_opening) {\n    if (typeof valve_opening !== \"number\") {\n        throw new Error(\"valve_opening must be a number\");\n    }\n    if (valve_opening < 0 || valve_opening > 100) {\n        throw new Error(\"valve_opening must be between 0 and 100\");\n    }\n\n    var buffer = new Buffer(3);\n    buffer.writeUInt8(0xff);\n    buffer.writeUInt8(0xb4);\n    buffer.writeUInt8(valve_opening);\n    return buffer.toBytes();\n}\n\n/**\n * valve calibration\n * @param {number} valve_calibration\n * @example { \"valve_calibration\": 1 }\n */\nfunction setValveCalibration(valve_calibration) {\n    var valve_calibration_values = [0, 1];\n    if (valve_calibration_values.indexOf(valve_calibration) == -1) {\n        throw new Error(\"valve_calibration must be one of \" + valve_calibration_values.join(\", \"));\n    }\n\n    if (valve_calibration === 0) {\n        return [];\n    }\n    return [0xff, 0xad, 0xff];\n}\n\n/**\n * valve control algorithm\n * @param {string} valve_control_algorithm values: (0: rate, 1: pid)\n * @example { \"valve_control_algorithm\": 0 }\n */\nfunction setValveControlAlgorithm(valve_control_algorithm) {\n    var valve_control_algorithm_values = [0, 1];\n    if (valve_control_algorithm_values.indexOf(valve_control_algorithm) == -1) {\n        throw new Error(\"valve_control_algorithm must be one of \" + valve_control_algorithm_values.join(\", \"));\n    }\n\n    var buffer = new Buffer(3);\n    buffer.writeUInt8(0xff);\n    buffer.writeUInt8(0xac);\n    buffer.writeUInt8(valve_control_algorithm);\n    return buffer.toBytes();\n}\n\n/**\n * freeze protection configuration\n * @param {number} enable\n * @param {number} temperature uint: Celsius\n * @example { \"freeze_protection_config\": { \"enable\": 1, \"temperature\": 5 } }\n * @example { \"freeze_protection_config\": { \"enable\": 0 } }\n */\nfunction setFreezeProtection(enable, temperature) {\n    var freeze_protection_enable_values = [0, 1];\n    if (freeze_protection_enable_values.indexOf(enable) == -1) {\n        throw new Error(\"freeze_protection_config.enable must be one of \" + freeze_protection_enable_values.join(\", \"));\n    }\n    if (enable && typeof temperature !== \"number\") {\n        throw new Error(\"freeze_protection_config.temperature must be a number\");\n    }\n\n    var buffer = new Buffer(5);\n    buffer.writeUInt8(0xff);\n    buffer.writeUInt8(0xb0);\n    buffer.writeUInt8(enable);\n    buffer.writeInt16LE(temperature * 10); // temperature\n    return buffer.toBytes();\n}\n\n/**\n * child lock configuration\n * @param {number} enable values: (0: disable, 1: enable)\n * @example { \"child_lock_config\": { \"enable\": 1 } }\n */\nfunction setChildLockEnable(enable) {\n    var child_lock_enable_values = [0, 1];\n    if (child_lock_enable_values.indexOf(enable) == -1) {\n        throw new Error(\"child_lock_config.enable must be one of \" + child_lock_enable_values.join(\", \"));\n    }\n\n    var buffer = new Buffer(3);\n    buffer.writeUInt8(0xff);\n    buffer.writeUInt8(0x25);\n    buffer.writeUInt8(enable);\n    return buffer.toBytes();\n}\n\nfunction Buffer(size) {\n    this.buffer = new Array(size);\n    this.offset = 0;\n\n    for (var i = 0; i < size; i++) {\n        this.buffer[i] = 0;\n    }\n}\n\nBuffer.prototype._write = function (value, byteLength, isLittleEndian) {\n    for (var index = 0; index < byteLength; index++) {\n        var shift = isLittleEndian ? index << 3 : (byteLength - 1 - index) << 3;\n        this.buffer[this.offset + index] = (value & (0xff << shift)) >> shift;\n    }\n};\n\nBuffer.prototype.writeUInt8 = function (value) {\n    this._write(value, 1, true);\n    this.offset += 1;\n};\n\nBuffer.prototype.writeInt8 = function (value) {\n    this._write(value < 0 ? value + 0x100 : value, 1, true);\n    this.offset += 1;\n};\n\nBuffer.prototype.writeUInt16LE = function (value) {\n    this._write(value, 2, true);\n    this.offset += 2;\n};\n\nBuffer.prototype.writeInt16LE = function (value) {\n    this._write(value < 0 ? value + 0x10000 : value, 2, true);\n    this.offset += 2;\n};\n\nBuffer.prototype.writeUInt32LE = function (value) {\n    this._write(value, 4, true);\n    this.offset += 4;\n};\n\nBuffer.prototype.writeInt32LE = function (value) {\n    this._write(value < 0 ? value + 0x100000000 : value, 4, true);\n    this.offset += 4;\n};\n\nBuffer.prototype.toBytes = function () {\n    return this.buffer;\n};\n\nconst payload = msg.payload\nconst data = milesightDeviceEncode(payload)\nmsg.encoded = data\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":230,"y":80,"wires":[["ac2d6efe.cb45c8"]]},{"id":"e5770610.7345c8","type":"mqtt out","z":"c4cffc88.53d56","name":"","topic":"auftrag/0000/buero-os/lora/downlink/0000000000000000","qos":"0","retain":"false","broker":"a46c266.466e5d8","x":1110,"y":80,"wires":[]},{"id":"ac2d6efe.cb45c8","type":"function","z":"c4cffc88.53d56","name":"b64 Encoder","func":"function toHexString(byteArray) {\n  return Array.from(byteArray, function(byte) {\n    return ('0' + (byte & 0xFF).toString(16)).slice(-2);\n  }).join('')\n}\n\n//Buffer.from(toHexString(msg.encoded).toUpperCase()).toString('base64')\nconst hex = toHexString(msg.encoded)\nvar base64String = Buffer.from(hex, 'hex').toString('base64')\n\nmsg.payload =base64String\nmsg.hex = hex\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":450,"y":80,"wires":[["dceb0acd.f9c44"]]},{"id":"dceb0acd.f9c44","type":"function","z":"c4cffc88.53d56","name":"make command","func":"const command = {\"confirmed\": true, \"fport\": 85, \"data\": msg.payload}\nmsg.payload = command\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":660,"y":80,"wires":[["aa869a8d.895e38"]]},{"id":"aa869a8d.895e38","type":"json","z":"c4cffc88.53d56","name":"","property":"payload","action":"","pretty":false,"x":810,"y":80,"wires":[["e5770610.7345c8"]]},{"id":"a46c266.466e5d8","type":"mqtt-broker","name":"","broker":"192.168.125.55","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"8204dc43.f28b1","type":"LoRa Input","z":"60646e91.f39128","name":"","devEUI":"","extendedField":"","x":180,"y":1000,"wires":[["8f75c309.8c4aa"]]},{"id":"ace10cbe.bfdcd8","type":"function","z":"60646e91.f39128","name":"Payload Decoder","func":"/**\n * Payload Decoder\n *\n * Copyright 2024 Milesight IoT\n *\n * @product WT101\n */\n// Chirpstack v4\nfunction decodeUplink(input) {\n    var decoded = milesightDeviceDecode(input.bytes);\n    return { data: decoded };\n}\n\n// Chirpstack v3\nfunction Decode(fPort, bytes) {\n    return milesightDeviceDecode(bytes);\n}\n\n// The Things Network\nfunction Decoder(bytes, port) {\n    return milesightDeviceDecode(bytes);\n}\n\nfunction milesightDeviceDecode(bytes) {\n    var decoded = {};\n\n    for (var i = 0; i < bytes.length; ) {\n        var channel_id = bytes[i++];\n        var channel_type = bytes[i++];\n        // BATTERY\n        if (channel_id === 0x01 && channel_type === 0x75) {\n            decoded.battery = bytes[i];\n            i += 1;\n        }\n        // TEMPERATURE\n        else if (channel_id === 0x03 && channel_type === 0x67) {\n            decoded.temperature = readInt16LE(bytes.slice(i, i + 2)) / 10;\n            i += 2;\n        }\n        // TEMPERATURE TARGET\n        else if (channel_id === 0x04 && channel_type === 0x67) {\n            decoded.temperature_target = readInt16LE(bytes.slice(i, i + 2)) / 10;\n            i += 2;\n        }\n        // VALVE OPENING\n        else if (channel_id === 0x05 && channel_type === 0x92) {\n            decoded.valve_opening = readUInt8(bytes[i]);\n            i += 1;\n        }\n        // INSTALLATION STATUS\n        else if (channel_id === 0x06 && channel_type === 0x00) {\n            decoded.tamper_status = bytes[i] === 0 ? \"installed\" : \"uninstalled\";\n            i += 1;\n        }\n        // FEENSTRATION STATUS\n        else if (channel_id === 0x07 && channel_type === 0x00) {\n            decoded.window_detection = bytes[i] === 0 ? \"normal\" : \"open\";\n            i += 1;\n        }\n        // MOTOR STORKE CALIBRATION STATUS\n        else if (channel_id === 0x08 && channel_type === 0xe5) {\n            decoded.motor_calibration_result = readMotorCalibration(bytes[i]);\n            i += 1;\n        }\n        // MOTOR STROKE\n        else if (channel_id === 0x09 && channel_type === 0x90) {\n            decoded.motor_storke = readUInt16LE(bytes.slice(i, i + 2));\n            i += 2;\n        }\n        // FROST PROTECTION\n        else if (channel_id === 0x0a && channel_type === 0x00) {\n            decoded.freeze_protection = bytes[i] === 0 ? \"normal\" : \"triggered\";\n            i += 1;\n        }\n        // MOTOR CURRENT POSTION\n        else if (channel_id === 0x0b && channel_type === 0x90) {\n            decoded.motor_position = readUInt16LE(bytes.slice(i, i + 2));\n            i += 2;\n        } else {\n            break;\n        }\n    }\n\n    return decoded;\n}\n\nfunction readUInt8(bytes) {\n    return bytes & 0xff;\n}\n\nfunction readInt8(bytes) {\n    var ref = readUInt8(bytes);\n    return ref > 0x7f ? ref - 0x100 : ref;\n}\n\nfunction readUInt16LE(bytes) {\n    var value = (bytes[1] << 8) + bytes[0];\n    return value & 0xffff;\n}\n\nfunction readInt16LE(bytes) {\n    var ref = readUInt16LE(bytes);\n    return ref > 0x7fff ? ref - 0x10000 : ref;\n}\n\nfunction readMotorCalibration(type) {\n    switch (type) {\n        case 0x00:\n            return \"success\";\n        case 0x01:\n            return \"fail: out of range\";\n        case 0x02:\n            return \"fail: uninstalled\";\n        case 0x03:\n            return \"calibration cleared\";\n        case 0x04:\n            return \"temperature control disabled\";\n        default:\n            return \"unknown\";\n    }\n}\nvar b64string = msg.payload;\nvar buf = Buffer.from(b64string, 'base64'); // Ta-da\nmsg.payload = milesightDeviceDecode(buf)\n\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":570,"y":1000,"wires":[["4e5270f8.a96a7","b2d08583.9d4f"]]},{"id":"4e5270f8.a96a7","type":"debug","z":"60646e91.f39128","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":810,"y":980,"wires":[]},{"id":"87db4bc0.b578e","type":"function","z":"60646e91.f39128","name":"temperature_target 20","func":"msg.payload = {\n    \"temperature_target\": 20,\n    \"temperature_error\": 1\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":380,"y":1360,"wires":[["48da41e4.f3fd4"]]},{"id":"13e367d1.4677c8","type":"inject","z":"60646e91.f39128","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":1360,"wires":[["87db4bc0.b578e"]]},{"id":"8f75c309.8c4aa","type":"Device Filter","z":"60646e91.f39128","name":"","eui":"0000000000000000","x":370,"y":1000,"wires":[["ace10cbe.bfdcd8"]]},{"id":"d7c74379.aea728","type":"function","z":"60646e91.f39128","name":"temperature_target 10","func":"msg.payload = {\n    \"temperature_target\": 10,\n    \"temperature_error\": 1\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":380,"y":1320,"wires":[["48da41e4.f3fd4"]]},{"id":"4ae526fd.7c69a","type":"inject","z":"60646e91.f39128","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":1320,"wires":[["d7c74379.aea728"]]},{"id":"48da41e4.f3fd4","type":"subflow:c4cffc88.53d56","z":"60646e91.f39128","name":"","env":[],"x":750,"y":1340,"wires":[]},{"id":"b56616f7.b2b25","type":"function","z":"60646e91.f39128","name":"temperature_control 0","func":"msg.payload = {\n    \"temperature_control\": {\"enable\": 0}\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":380,"y":1200,"wires":[["48da41e4.f3fd4"]]},{"id":"1dd9f7c0.6006b8","type":"inject","z":"60646e91.f39128","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":1200,"wires":[["b56616f7.b2b25"]]},{"id":"ecf5c7ef.93a4b","type":"function","z":"60646e91.f39128","name":"temperature_control1","func":"msg.payload = {\n    \"temperature_control\": {\"enable\": 1}\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":380,"y":1160,"wires":[["48da41e4.f3fd4"]]},{"id":"ed0e1d68.5fab68","type":"inject","z":"60646e91.f39128","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":1160,"wires":[["ecf5c7ef.93a4b"]]},{"id":"a11e0d00.0fce","type":"inject","z":"60646e91.f39128","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":1520,"wires":[[]]},{"id":"a29b59aa.ebdd18","type":"function","z":"60646e91.f39128","name":"valve_opening 100","func":"msg.payload = {\n    \"valve_opening\": 100\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":370,"y":1480,"wires":[["48da41e4.f3fd4"]]},{"id":"7fa43a65.c95954","type":"inject","z":"60646e91.f39128","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":1480,"wires":[["a29b59aa.ebdd18"]]},{"id":"ccb8caf0.df0ce","type":"function","z":"60646e91.f39128","name":"valve_opening 0","func":"msg.payload = {\n    \"valve_opening\": 0\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":370,"y":1520,"wires":[["48da41e4.f3fd4"]]},{"id":"f2869c0c.3b801","type":"inject","z":"60646e91.f39128","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":1600,"wires":[["b0b9e6a0.1ff6e8"]]},{"id":"b0b9e6a0.1ff6e8","type":"function","z":"60646e91.f39128","name":"valve_calibration 1","func":"msg.payload = {\n    \"valve_calibration\": 1\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":370,"y":1600,"wires":[["48da41e4.f3fd4"]]},{"id":"332a197b.4aae6e","type":"inject","z":"60646e91.f39128","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":140,"y":1740,"wires":[["8cfbe813.96b9d8"]]},{"id":"8cfbe813.96b9d8","type":"function","z":"60646e91.f39128","name":"report_interval 1","func":"msg.payload = {\n    \"report_interval\": 1\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":350,"y":1740,"wires":[["48da41e4.f3fd4"]]},{"id":"b6f2768c.487318","type":"inject","z":"60646e91.f39128","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":140,"y":1780,"wires":[["1ff168b6.0e6297"]]},{"id":"1ff168b6.0e6297","type":"function","z":"60646e91.f39128","name":"report_interval 10","func":"msg.payload = {\n    \"report_interval\": 10\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":350,"y":1780,"wires":[["48da41e4.f3fd4"]]},{"id":"2510270b.f72478","type":"inject","z":"60646e91.f39128","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":1640,"wires":[["dc1f500c.f66be8"]]},{"id":"dc1f500c.f66be8","type":"function","z":"60646e91.f39128","name":"valve_calibration 0","func":"msg.payload = {\n    \"valve_calibration\": 0\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":370,"y":1640,"wires":[["48da41e4.f3fd4"]]},{"id":"e2376af1.913348","type":"function","z":"60646e91.f39128","name":"temperature_target 28","func":"msg.payload = {\n    \"temperature_target\": 28,\n    \"temperature_error\": 1\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":380,"y":1400,"wires":[["48da41e4.f3fd4"]]},{"id":"88900e1b.5e8558","type":"inject","z":"60646e91.f39128","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":1400,"wires":[["e2376af1.913348"]]},{"id":"b2d08583.9d4f","type":"change","z":"60646e91.f39128","name":"","rules":[{"t":"set","p":"last","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":830,"y":1040,"wires":[[]]}]

I honestly do not know what to think (not about your project / code I hasten to add).

Building Node-Red into a communications gateway seems 'odd' to me - not sure why but maybe it's my inner Linux :astonished: creeping out - create a tool to do one job and do it well.

To have an industrial piece of kit that is running out of date software (Released 2021) is concerning esp with one that has wireless access (OK I do not know of any LoRa vulnerabilities but that does not mean they do not exist).

I can see the selling points:

  • Pre-process the data before it hits the main network
  • One box so less maintenance
  • One supplier

but still bothered so I'll not surf their offerings I'm afraid.

Yeah, you are right.

Luckily, you can configure the mqtt server, so you can use your own node-red instance :slight_smile:

That's more comforting - I kinda like the 'one box' but it just bugs me... :slightly_smiling_face: