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":[[]]}]