Skip to content

Instantly share code, notes, and snippets.

@fensoft
Created December 18, 2025 23:39
Show Gist options
  • Select an option

  • Save fensoft/88175fe7c7230ee45c8171a488c70780 to your computer and use it in GitHub Desktop.

Select an option

Save fensoft/88175fe7c7230ee45c8171a488c70780 to your computer and use it in GitHub Desktop.
[
{
"id": "fb5296f3c4de2a23",
"type": "subflow",
"name": "Create HA Helpers",
"info": "",
"category": "HA Actions",
"in": [
{
"x": 84,
"y": 96,
"wires": [
{
"id": "fc18bdbb57e2fd63"
}
]
}
],
"out": [],
"env": [
{
"name": "serverName",
"type": "str",
"value": "Home Assistant",
"ui": {
"label": {
"en-US": "HA Server Name"
},
"type": "input",
"opts": {
"types": [
"str"
]
}
}
},
{
"name": "helpers",
"type": "json",
"value": "[]",
"ui": {
"label": {
"en-US": "Helpers"
},
"type": "input",
"opts": {
"types": [
"json"
]
}
}
}
],
"meta": {},
"color": "#46B1EF",
"status": {
"x": 246,
"y": 48,
"wires": [
{
"id": "ea6dc8ad6d9ece17",
"port": 0
}
]
}
},
{
"id": "fc18bdbb57e2fd63",
"type": "function",
"z": "fb5296f3c4de2a23",
"name": "process helpers",
"func": "const serverName = toCamelCase(env.get('serverName'));\nconst haServer = global.get(\"homeassistant\")[serverName];\nif(!haServer) {\n node.error(\"Invalid HA server name\");\n return;\n}\nconst states = haServer.states;\nconst helpers = env.get(\"helpers\");\n\nhelpers.forEach(h => {\n const entityId = `${h.type}.${h.id}`;\n if(!states[entityId]) {\n const {id, type, ...data} = h;\n const apiData = {\n entity: h,\n payload: { \n data \n }\n };\n apiData.payload.data.type = `${type}/create`;\n \n node.send(apiData);\n node.status({text: `Creating ${entityId}`});\n }\n});\n\nnode.done();\n\nfunction toCamelCase(str) {\n return str.replace(/(?:^\\w|[A-Z]|\\b\\w|\\s+)/g, (match, index) => {\n if (+match === 0) return '';\n return index === 0 ? match.toLowerCase() : match.toUpperCase();\n });\n}",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 224,
"y": 96,
"wires": [
[
"2b8e82c6d93d38e5"
]
]
},
{
"id": "2b8e82c6d93d38e5",
"type": "ha-api",
"z": "fb5296f3c4de2a23",
"name": "create helper",
"server": "e89e2dad.1bf6",
"version": 1,
"debugenabled": false,
"protocol": "websocket",
"method": "get",
"path": "",
"data": "",
"dataType": "json",
"responseType": "json",
"outputProperties": [
{
"property": "payload",
"propertyType": "msg",
"value": "",
"valueType": "results"
}
],
"x": 390,
"y": 96,
"wires": [
[
"767624d22ee0c819"
]
]
},
{
"id": "372232586f5e138f",
"type": "ha-api",
"z": "fb5296f3c4de2a23",
"name": "rename helper entity id",
"server": "e89e2dad.1bf6",
"version": 1,
"debugenabled": false,
"protocol": "websocket",
"method": "get",
"path": "",
"data": "{\t \"type\":\"config/entity_registry/update\",\t \"entity_id\": entity.type & \".\" & payload.id,\t \"new_entity_id\": entity.type & \".\" & entity.id\t}",
"dataType": "jsonata",
"responseType": "json",
"outputProperties": [
{
"property": "payload",
"propertyType": "msg",
"value": "",
"valueType": "results"
}
],
"x": 788,
"y": 96,
"wires": [
[]
]
},
{
"id": "767624d22ee0c819",
"type": "switch",
"z": "fb5296f3c4de2a23",
"name": "need to rename?",
"property": "payload.id",
"propertyType": "msg",
"rules": [
{
"t": "neq",
"v": "entity.id",
"vt": "msg"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 570,
"y": 96,
"wires": [
[
"372232586f5e138f",
"815fe31e9d239e53"
]
]
},
{
"id": "ea6dc8ad6d9ece17",
"type": "status",
"z": "fb5296f3c4de2a23",
"name": "",
"scope": [
"fc18bdbb57e2fd63",
"815fe31e9d239e53"
],
"x": 124,
"y": 48,
"wires": [
[]
]
},
{
"id": "815fe31e9d239e53",
"type": "function",
"z": "fb5296f3c4de2a23",
"name": "rename status",
"func": "const oldEntityId = `${msg.entity.type}.${msg.payload.id}`;\nconst newEntityId = `${msg.entity.type}.${msg.entity.id}`;\n\nnode.status({text: `Renaming ${oldEntityId} to ${newEntityId}`});",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 768,
"y": 144,
"wires": [
[]
]
},
{
"id": "bae136fa5a77790e",
"type": "subflow",
"name": "On Restart",
"info": "",
"category": "",
"in": [],
"out": [
{
"x": 320,
"y": 320,
"wires": [
{
"id": "c67631f89e6451f4",
"port": 0
}
]
}
],
"env": [],
"meta": {},
"color": "#DDAA99"
},
{
"id": "4a91394f75614da1",
"type": "server-events",
"z": "bae136fa5a77790e",
"name": "",
"server": "e89e2dad.1bf6",
"version": 3,
"exposeAsEntityConfig": "",
"eventType": "home_assistant_client",
"eventData": "",
"waitForRunning": false,
"outputProperties": [
{
"property": "payload",
"propertyType": "msg",
"value": "",
"valueType": "eventData"
}
],
"x": 140,
"y": 220,
"wires": [
[
"95e9d47ed2be5007"
]
]
},
{
"id": "95e9d47ed2be5007",
"type": "switch",
"z": "bae136fa5a77790e",
"name": "running?",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "running",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 352,
"y": 220,
"wires": [
[
"4b7a32290de02cf7"
]
]
},
{
"id": "648dd9ac108befdc",
"type": "api-call-service",
"z": "bae136fa5a77790e",
"name": "",
"server": "e89e2dad.1bf6",
"version": 7,
"debugenabled": false,
"action": "input_boolean.turn_off",
"floorId": [],
"areaId": [],
"deviceId": [],
"entityId": [
"input_boolean.home_assistant_restarted"
],
"labelId": [],
"data": "",
"dataType": "json",
"mergeContext": "",
"mustacheAltTags": false,
"outputProperties": [],
"queue": "none",
"blockInputOverrides": false,
"domain": "input_boolean",
"service": "turn_off",
"x": 758,
"y": 220,
"wires": [
[
"3754cbf709193071"
]
]
},
{
"id": "4b7a32290de02cf7",
"type": "api-current-state",
"z": "bae136fa5a77790e",
"name": "has restarted?",
"server": "e89e2dad.1bf6",
"version": 3,
"outputs": 2,
"halt_if": "on",
"halt_if_type": "str",
"halt_if_compare": "is",
"entity_id": "input_boolean.home_assistant_restarted",
"state_type": "str",
"blockInputOverrides": false,
"outputProperties": [],
"for": 0,
"forType": "num",
"forUnits": "minutes",
"x": 516,
"y": 220,
"wires": [
[
"648dd9ac108befdc"
],
[]
]
},
{
"id": "f89ae68a99bb4e65",
"type": "server-state-changed",
"z": "bae136fa5a77790e",
"name": "restarted?",
"server": "e89e2dad.1bf6",
"version": 6,
"outputs": 2,
"exposeAsEntityConfig": "",
"entities": {
"entity": [
"input_boolean.home_assistant_restarted"
],
"substring": [],
"regex": []
},
"outputInitially": false,
"stateType": "str",
"ifState": "on",
"ifStateType": "str",
"ifStateOperator": "is",
"outputOnlyOnStateChange": false,
"for": 0,
"forType": "num",
"forUnits": "minutes",
"ignorePrevStateNull": false,
"ignorePrevStateUnknown": false,
"ignorePrevStateUnavailable": false,
"ignoreCurrentStateUnknown": false,
"ignoreCurrentStateUnavailable": false,
"outputProperties": [],
"x": 528,
"y": 268,
"wires": [
[
"648dd9ac108befdc"
],
[]
]
},
{
"id": "b5993f580d9eb5b8",
"type": "comment",
"z": "bae136fa5a77790e",
"name": "Home Assistant Restarted",
"info": "",
"x": 130,
"y": 124,
"wires": []
},
{
"id": "3754cbf709193071",
"type": "link out",
"z": "bae136fa5a77790e",
"name": "Home Assistant Restarted",
"links": [
"c67631f89e6451f4"
],
"x": 1010,
"y": 220,
"wires": [],
"l": true
},
{
"id": "c67631f89e6451f4",
"type": "link in",
"z": "bae136fa5a77790e",
"name": "Home Assistant Restarted",
"links": [
"3754cbf709193071"
],
"x": 130,
"y": 316,
"wires": [
[]
],
"l": true
},
{
"id": "bcfea616e335904d",
"type": "inject",
"z": "bae136fa5a77790e",
"name": "Click to create",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 106,
"y": 172,
"wires": [
[
"447721875af4431a",
"5a92fd8241edd308"
]
]
},
{
"id": "447721875af4431a",
"type": "ha-api",
"z": "bae136fa5a77790e",
"name": "HA automation",
"server": "e89e2dad.1bf6",
"version": 1,
"debugenabled": false,
"protocol": "http",
"method": "post",
"path": "/api/config/automation/config/nrrestartnotification",
"data": "{\t \"alias\": \"Home Assistant Restart\",\t \"description\": \"\",\t \"trigger\": [\t {\t \"platform\": \"homeassistant\",\t \"event\": \"start\" \t } \t ],\t \"condition\": [],\t \"action\": [\t {\t \"service\": \"input_boolean.turn_on\",\t \"data\": {\t \"entity_id\": \"input_boolean.home_assistant_restarted\" \t }\t } \t ],\t \"mode\": \"single\" \t}",
"dataType": "jsonata",
"responseType": "json",
"outputProperties": [],
"x": 372,
"y": 124,
"wires": [
[]
]
},
{
"id": "5a92fd8241edd308",
"type": "subflow:fb5296f3c4de2a23",
"z": "bae136fa5a77790e",
"name": "",
"env": [
{
"name": "serverName",
"value": "Home Assistant Working",
"type": "str"
},
{
"name": "helpers",
"value": "[{\"id\":\"home_assistant_restarted\",\"type\":\"input_boolean\",\"name\":\"Home Assistant Restarted\",\"icon\":\"mdi:restart-alert\"}]",
"type": "json"
}
],
"x": 382,
"y": 172,
"wires": []
},
{
"id": "e89e2dad.1bf6",
"type": "server",
"name": "Home Assistant Working",
"addon": true,
"rejectUnauthorizedCerts": true,
"ha_boolean": "",
"connectionDelay": false,
"cacheJson": false,
"heartbeat": false,
"heartbeatInterval": "",
"entitySelector": "id",
"statusSeparator": "",
"enableGlobalContextStore": false
},
{
"id": "103b1fd4c70bd5ad",
"type": "tab",
"label": "Max! 2 MQTT",
"disabled": false,
"info": "",
"env": []
},
{
"id": "fd3ce805ed926c80",
"type": "inject",
"z": "103b1fd4c70bd5ad",
"name": "timer",
"props": [
{
"p": "reason",
"v": "repeat",
"vt": "str"
}
],
"repeat": "120",
"crontab": "",
"once": true,
"onceDelay": "1",
"topic": "",
"x": 90,
"y": 540,
"wires": [
[
"248a31587c7960fe"
]
]
},
{
"id": "7b8330524ea2f4c4",
"type": "mqtt out",
"z": "103b1fd4c70bd5ad",
"name": "mqtt boiler",
"topic": "cmnd/tasmota_F4295B/POWER",
"qos": "0",
"retain": "false",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "d7efe4ef4af81eb9",
"x": 770,
"y": 320,
"wires": []
},
{
"id": "9fabcb15eb75a432",
"type": "function",
"z": "103b1fd4c70bd5ad",
"name": "compute action",
"func": "const rooms = Object.values(msg.payload).filter(v => v.device_type === 3);\n\n// calculate total temperature deficit (\"miss\")\nconst miss = rooms.reduce((sum, v) => {\n return sum + ((v.temp < v.setpoint && v.room_name !== 'WC') ? (v.setpoint - v.temp) : 0);\n}, 0);\n\n// determine desired action\nlet desiredAction = null;\nif (miss >= 1.2) desiredAction = 'ON';\nelse if (miss <= 0.5) desiredAction = 'OFF';\n\n// get last state and time from context\nconst lastAction = context.get('lastAction') || null;\nconst lastChange = context.get('lastChange') || 0;\nconst now = Date.now();\n\n// if less than 10 min since last change, keep same state\nif (lastAction && (now - lastChange < 5 * 60 * 1000)) {\n desiredAction = lastAction;\n} else if (desiredAction !== lastAction && desiredAction !== null) {\n // record new state and timestamp\n context.set('lastAction', desiredAction);\n context.set('lastChange', now);\n}\n\n// get current values after possible update\nconst currentAction = context.get('lastAction') || desiredAction;\nconst currentChange = context.get('lastChange') || now;\n\n// build per-room status\nconst res = Object.fromEntries(\n rooms.map(v => {\n const name = v.room_name.replace(\" \", \"_\");\n const status = (currentAction === 'ON' && v.temp < v.setpoint && v.room_name !== 'WC')\n ? 'heat'\n : 'off';\n return [name, { status }];\n })\n);\n\n// make readable timestamp\nconst readableTime = new Date(currentChange).toLocaleString();\n\nreturn [\n { payload: currentAction, heat: miss },\n { payload: res },\n { payload: { readableTime, lastAction: currentAction } }\n];\n\n// var res = {};\n// var miss = 0;\n// for (const [key, value] of Object.entries(msg.payload)) {\n// if (value.device_type == 3) {\n// name = value.room_name.replace(\" \", \"_\");// + \"_\" + value.device_name.replace(\" \", \"_\");\n// if (value.temp < value.setpoint) {\n// if (value.room_name != 'WC')\n// miss += value.setpoint - value.temp;\n// }\n// res[name] = { \"status\": \"off\" };\n// }\n// }\n\n// var action = null;\n// if (miss <= 0.5) {\n// action = 'OFF';\n// }\n// if (miss >= 1.2) {\n// action = 'ON';\n// for (const [key, value] of Object.entries(msg.payload)) {\n// if (value.device_type == 3) {\n// name = value.room_name.replace(\" \", \"_\");// + \"_\" + value.device_name.replace(\" \", \"_\")\n// if (value.temp < value.setpoint && value.room_name != 'WC') {\n// status = 'heat';\n// } else {\n// status = 'off';\n// }\n// res[name] = { \"status\": status };\n// }\n// }\n// }\n// return [ { 'payload': action, 'heat': miss }, { \"payload\": res } ];",
"outputs": 3,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 440,
"y": 460,
"wires": [
[
"7b8330524ea2f4c4",
"87831a7d8384ba61",
"eaa0ef6ec5828c4b",
"91688cf578b02dfa"
],
[
"f1d30ee021d42cf6"
],
[]
]
},
{
"id": "3355f9d8134bb8d5",
"type": "split",
"z": "103b1fd4c70bd5ad",
"name": "",
"splt": "\\n",
"spltType": "str",
"arraySplt": 1,
"arraySpltType": "len",
"stream": false,
"addname": "",
"x": 630,
"y": 580,
"wires": [
[
"d09de1b4a1e90b50"
]
]
},
{
"id": "2ebfae9701dd6b68",
"type": "function",
"z": "103b1fd4c70bd5ad",
"name": "decode",
"func": "var res = {};\nfor (const [key, value] of Object.entries(msg.payload)) {\n if (value.device_type) {\n name = value.room_name.replace(\" \", \"_\") + \"_\" + value.device_name.replace(\" \", \"_\");\n if (!(value.device_type in res))\n res[value.device_type] = {};\n res[value.device_type][name] = value;\n }\n}\nreturn [ { \"payload\": res[1] }, { \"payload\": res[3] }, { \"payload\": res[4] }, res ];",
"outputs": 4,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 420,
"y": 600,
"wires": [
[
"f3a618cb9b217476"
],
[
"3355f9d8134bb8d5"
],
[
"da37c30786c72470"
],
[]
]
},
{
"id": "f1d30ee021d42cf6",
"type": "split",
"z": "103b1fd4c70bd5ad",
"name": "",
"splt": "\\n",
"spltType": "str",
"arraySplt": 1,
"arraySpltType": "len",
"stream": false,
"addname": "",
"x": 630,
"y": 480,
"wires": [
[
"835b0a70c0092e13"
]
]
},
{
"id": "835b0a70c0092e13",
"type": "function",
"z": "103b1fd4c70bd5ad",
"name": "thermostat state",
"func": "msg.topic = 'maxcube2mqtt/thermostat/' + msg.parts.key;\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 780,
"y": 480,
"wires": [
[
"f855b4166b9fe922"
]
]
},
{
"id": "f3a618cb9b217476",
"type": "split",
"z": "103b1fd4c70bd5ad",
"name": "",
"splt": "\\n",
"spltType": "str",
"arraySplt": 1,
"arraySpltType": "len",
"stream": false,
"addname": "",
"x": 630,
"y": 540,
"wires": [
[
"221d232e309d8974"
]
]
},
{
"id": "da37c30786c72470",
"type": "split",
"z": "103b1fd4c70bd5ad",
"name": "",
"splt": "\\n",
"spltType": "str",
"arraySplt": 1,
"arraySpltType": "len",
"stream": false,
"addname": "",
"x": 630,
"y": 620,
"wires": [
[
"b64957542172ae46"
]
]
},
{
"id": "87831a7d8384ba61",
"type": "function",
"z": "103b1fd4c70bd5ad",
"name": "boiler total",
"func": "var msgo = {};\nmsgo.payload = msg;\nmsgo.payload.heat = msgo.payload.heat.toFixed(1)\nmsgo.topic = 'homeassistant/sensor/maxcube_heat';\nmsgo.payload.config = {}\nmsgo.payload.config[\"~\"] = msgo.topic;\nmsgo.payload.config[\"uniq_id\"] = \"maxcube_heat\";\nmsgo.payload.config[\"name\"] = \"Chaudière Total\";\nmsgo.payload.config[\"state_topic\"] = \"~/heat\";\nmsgo.payload.config[\"unit_of_measurement\"] = \"°\";\nreturn msgo;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 770,
"y": 440,
"wires": [
[
"f855b4166b9fe922"
]
]
},
{
"id": "9288a4f985382537",
"type": "mqtt out",
"z": "103b1fd4c70bd5ad",
"name": "mqtt",
"topic": "",
"qos": "",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "d7efe4ef4af81eb9",
"x": 1190,
"y": 440,
"wires": []
},
{
"id": "f855b4166b9fe922",
"type": "split",
"z": "103b1fd4c70bd5ad",
"name": "",
"splt": "\\n",
"spltType": "str",
"arraySplt": 1,
"arraySpltType": "len",
"stream": false,
"addname": "key",
"x": 950,
"y": 440,
"wires": [
[
"9f45cc50a1240208"
]
]
},
{
"id": "9f45cc50a1240208",
"type": "function",
"z": "103b1fd4c70bd5ad",
"name": "suffix",
"func": "msg.topic = msg.topic + '/' + msg.key;\ndelete msg.key;\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1070,
"y": 440,
"wires": [
[
"9288a4f985382537"
]
]
},
{
"id": "29949c9d2834f922",
"type": "comment",
"z": "103b1fd4c70bd5ad",
"name": "boiler",
"info": "boiler management.\nCompute total of missing degrees in each room and turn on the boiler if needed.",
"x": 410,
"y": 400,
"wires": []
},
{
"id": "af681e641dcfc3b7",
"type": "comment",
"z": "103b1fd4c70bd5ad",
"name": "mqtt receive",
"info": "send/receive to mqtt",
"x": 430,
"y": 540,
"wires": []
},
{
"id": "248a31587c7960fe",
"type": "maxcube out",
"z": "103b1fd4c70bd5ad",
"server": "9a07e5adcaeb56f7",
"singleMessage": true,
"x": 220,
"y": 540,
"wires": [
[
"9fabcb15eb75a432",
"2ebfae9701dd6b68"
]
]
},
{
"id": "eaa0ef6ec5828c4b",
"type": "function",
"z": "103b1fd4c70bd5ad",
"name": "boiler state",
"func": "// msg.payload = 'OFF';\n// if (msg.payload == null)\n// return null;\nvar msgo = {};\nmsgo.payload = {};\nmsgo.topic = 'homeassistant/binary_sensor/maxcube_state';\nmsgo.payload.state = msg.payload;\nmsgo.payload.config = {}\nmsgo.payload.config[\"~\"] = msgo.topic;\nmsgo.payload.config[\"uniq_id\"] = \"maxcube_state\";\nmsgo.payload.config[\"name\"] = \"Chaudière État\";\nmsgo.payload.config[\"state_topic\"] = \"~/state\";\nreturn msgo;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 770,
"y": 400,
"wires": [
[
"f855b4166b9fe922"
]
]
},
{
"id": "d09de1b4a1e90b50",
"type": "function",
"z": "103b1fd4c70bd5ad",
"name": "thermostat",
"func": "msg.topic = 'maxcube2mqtt/thermostat/' + msg.payload.room_name.replace(\" \", \"_\");\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 770,
"y": 580,
"wires": [
[
"5ba40e51ea31021b"
]
]
},
{
"id": "221d232e309d8974",
"type": "function",
"z": "103b1fd4c70bd5ad",
"name": "valve",
"func": "msg.topic = 'maxcube2mqtt/valve/' + msg.payload.room_name.replace(\" \", \"_\") + \"_\" + msg.payload.device_name.replace(\" \", \"_\");\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 750,
"y": 540,
"wires": [
[
"5ba40e51ea31021b"
]
]
},
{
"id": "5ba40e51ea31021b",
"type": "split",
"z": "103b1fd4c70bd5ad",
"name": "",
"splt": "\\n",
"spltType": "str",
"arraySplt": 1,
"arraySpltType": "len",
"stream": false,
"addname": "key",
"x": 910,
"y": 580,
"wires": [
[
"f834c4de1ba53a86"
]
]
},
{
"id": "f834c4de1ba53a86",
"type": "function",
"z": "103b1fd4c70bd5ad",
"name": "suffix",
"func": "msg.topic = msg.topic + '/' + msg.key;\ndelete msg.key;\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1030,
"y": 580,
"wires": [
[
"efcb1347e477233d"
]
]
},
{
"id": "b64957542172ae46",
"type": "function",
"z": "103b1fd4c70bd5ad",
"name": "window",
"func": "msg.topic = 'maxcube2mqtt/window/' + msg.payload.room_name.replace(\" \", \"_\") + \" / \" + msg.payload.device_name.replace(\" \", \"_\");\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 760,
"y": 620,
"wires": [
[
"5ba40e51ea31021b"
]
]
},
{
"id": "3d2761b7773ae8d5",
"type": "inject",
"z": "103b1fd4c70bd5ad",
"name": "on start",
"props": [
{
"p": "reason",
"v": "start",
"vt": "str"
}
],
"repeat": "3600",
"crontab": "",
"once": true,
"onceDelay": "15",
"topic": "",
"x": 160,
"y": 140,
"wires": [
[
"554700ca0a7b0288"
]
]
},
{
"id": "554700ca0a7b0288",
"type": "maxcube out",
"z": "103b1fd4c70bd5ad",
"server": "9a07e5adcaeb56f7",
"singleMessage": true,
"x": 320,
"y": 160,
"wires": [
[
"262018a32e7e029b"
]
]
},
{
"id": "262018a32e7e029b",
"type": "function",
"z": "103b1fd4c70bd5ad",
"name": "decode",
"func": "var res = {};\nfor (const [key, value] of Object.entries(msg.payload)) {\n if (value.device_type) {\n name = value.room_name.replace(\" \", \"_\") + \"_\" + value.device_name.replace(\" \", \"_\");\n if (!(value.device_type in res))\n res[value.device_type] = {};\n res[value.device_type][name] = value;\n }\n}\nreturn [ { \"payload\": res[1] }, { \"payload\": res[3] }, { \"payload\": res[4] }, res ];",
"outputs": 4,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 480,
"y": 160,
"wires": [
[
"45a48dfb50ec27a3"
],
[
"cf5b45a47dc5be43"
],
[
"1e0f3bbea406afcd"
],
[]
],
"outputLabels": [
"valve",
"thermostat",
"window",
""
]
},
{
"id": "45a48dfb50ec27a3",
"type": "split",
"z": "103b1fd4c70bd5ad",
"name": "",
"splt": "\\n",
"spltType": "str",
"arraySplt": 1,
"arraySpltType": "len",
"stream": false,
"addname": "",
"x": 650,
"y": 100,
"wires": [
[
"c4bef775bb3de4a4",
"3ec8ad146c23b99e"
]
]
},
{
"id": "c4bef775bb3de4a4",
"type": "function",
"z": "103b1fd4c70bd5ad",
"name": "valve",
"func": "var res = {}\nres.topic = 'homeassistant/climate/' + msg.payload.room_name.replace(\" \", \"_\") + \"-\" + msg.payload.device_name.replace(\" \", \"_\") + '/config';\nres.payload = {}\nres.payload[\"~\"] = 'maxcube2mqtt/valve/' + msg.payload.room_name.replace(\" \", \"_\") + \"_\" + msg.payload.device_name.replace(\" \", \"_\");\nres.payload[\"unique_id\"] = \"maxcube_valve_\" + msg.parts.key.replace(\" \", \"_\");\nres.payload[\"name\"] = \"Valve \" + msg.payload.room_name + \" / \" + msg.payload.device_name;\nres.payload[\"temp_cmd_t\"] = \"~/setpoint/set\";\nres.payload[\"temp_stat_t\"] = \"~/setpoint\";\nres.payload[\"curr_temp_t\"] = \"~/temp\";\nres.payload[\"min_temp\"] = \"12\";\nres.payload[\"max_temp\"] = \"25\";\nres.payload[\"temp_step\"] = \"0.5\";\nres.payload[\"availability_topic\"] = \"~/error\";\nres.payload[\"payload_available\"] = \"false\";\nres.payload[\"payload_not_available\"] = \"true\";\nres.payload[\"device\"] = {};\nres.payload.device[\"manufacturer\"] = \"ELV\";\nres.payload.device[\"model\"] = \"valve\";\nres.payload.device[\"name\"] = res.payload[\"name\"];\nres.payload.device[\"identifiers\"] = [ \"maxcube_\" + msg.payload.rf_address];\n// res.payload.device[\"connections\"] = [ [ \"mac\", msg.payload.rf_address ] ];\n// res.payload[\"origin\"] = {};\n// res.payload.origin[\"name\"] = \"maxcube\";\nreturn res;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 790,
"y": 60,
"wires": [
[
"3c3e46d6fee1be7b"
]
]
},
{
"id": "3c3e46d6fee1be7b",
"type": "mqtt out",
"z": "103b1fd4c70bd5ad",
"name": "mqtt",
"topic": "",
"qos": "",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "d7efe4ef4af81eb9",
"x": 1010,
"y": 160,
"wires": []
},
{
"id": "3ec8ad146c23b99e",
"type": "function",
"z": "103b1fd4c70bd5ad",
"name": "valve bat",
"func": "var res = {}\nres.topic = 'homeassistant/sensor/' + msg.payload.room_name.replace(\" \", \"_\") + \"-\" + msg.payload.device_name.replace(\" \", \"_\") + \"_battery/config\";\nres.payload = {}\nres.payload[\"~\"] = 'maxcube2mqtt/valve/' + msg.payload.room_name.replace(\" \", \"_\") + \"_\" + msg.payload.device_name.replace(\" \", \"_\");\nres.payload[\"uniq_id\"] = \"maxcube_\" + msg.parts.key.replace(\" \", \"_\") + \"_battery\";\nres.payload[\"name\"] = \"Valve \" + msg.payload.room_name + \" / \" + msg.payload.device_name + \" Battery\";\nres.payload[\"device_class\"] = \"battery\";\nres.payload[\"entity_category\"] = \"diagnostic\";\nres.payload[\"state_class\"] = \"measurement\";\nres.payload[\"state_topic\"] = \"~/battery_low\";\nres.payload[\"unit_of_measurement\"] = \"%\";\nres.payload[\"value_template\"] = '{{ 0 if value == \"true\" else 100 }}';\nres.payload[\"device\"] = {};\nres.payload.device[\"manufacturer\"] = \"ELV\";\nres.payload.device[\"model\"] = \"valve\";\nres.payload.device[\"name\"] = res.payload[\"name\"];\nres.payload.device[\"identifiers\"] = [ \"maxcube_\" + msg.payload.rf_address];\nreturn res;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 800,
"y": 100,
"wires": [
[
"3c3e46d6fee1be7b"
]
]
},
{
"id": "6ad662a6b39283fa",
"type": "function",
"z": "103b1fd4c70bd5ad",
"name": "thermostat ",
"func": "var res = {};\nres.topic = 'homeassistant/climate/' + msg.payload.room_name.replace(\" \", \"_\") + '/config';\nres.payload = {};\nres.payload[\"~\"] = 'maxcube2mqtt/thermostat/' + msg.payload.room_name.replace(\" \", \"_\");\nres.payload[\"uniq_id\"] = 'maxcube_' + msg.payload.room_name.replace(\" \", \"_\");\nres.payload[\"name\"] = 'Thermostat ' + msg.payload.room_name;\n\n// HVAC modes\nres.payload[\"modes\"] = [\"heat\", \"off\"];\nres.payload[\"mode_state_topic\"] = '~/status'; // must publish \"heat\" or \"off\"\nres.payload[\"mode_command_topic\"] = '~/status/set';// must accept \"heat\" or \"off\"\n\n// Temperature control\nres.payload[\"temp_cmd_t\"] = '~/setpoint/set';\nres.payload[\"temp_stat_t\"] = '~/setpoint';\nres.payload[\"curr_temp_t\"] = '~/temp';\nres.payload[\"min_temp\"] = 12; // numbers, not strings\nres.payload[\"max_temp\"] = 25;\nres.payload[\"temp_step\"] = 0.5;\nres.payload[\"temperature_unit\"] = 'C';\n\n// Presets (AUTO/MANUAL/BOOST)\nres.payload[\"preset_modes\"] = [\"AUTO\", \"MANUAL\", \"BOOST\"];\nres.payload[\"preset_mode_command_topic\"] = '~/mode/set';\nres.payload[\"preset_mode_state_topic\"] = '~/mode';\n\n// Availability via error topic (false == OK)\n// res.payload[\"availability_topic\"] = '~/error';\n// res.payload[\"payload_available\"] = 'false';\n// res.payload[\"payload_not_available\"] = 'true';\n\nres.payload[\"device\"] = {\n manufacturer: 'ELV',\n model: 'thermostat',\n name: res.payload[\"name\"],\n identifiers: ['maxcube_' + msg.payload.rf_address]\n};\nreturn res;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 810,
"y": 140,
"wires": [
[
"3c3e46d6fee1be7b"
]
]
},
{
"id": "794a0609769ac85c",
"type": "function",
"z": "103b1fd4c70bd5ad",
"name": "thermostat bat ",
"func": "// Battery \"percentage\" from low flag (0% or 100%), always available\nvar room = (msg.payload.room_name || '').replace(/\\s+/g, '_');\nvar key = (msg.parts.key || '').replace(/\\s+/g, '_');\n\nvar res = {};\nres.topic = 'homeassistant/sensor/' + key + '_battery/config';\nres.payload = {};\n\n// Base\nres.payload[\"~\"] = 'maxcube2mqtt/thermostat/' + room;\nres.payload[\"uniq_id\"] = 'maxcube_' + key + '_battery';\nres.payload[\"name\"] = 'Thermostat ' + msg.payload.room_name + ' / ' + msg.payload.device_name + ' Battery';\n\n// Sensor as 0%/100%\nres.payload[\"device_class\"] = 'battery';\nres.payload[\"entity_category\"] = 'diagnostic';\nres.payload[\"state_class\"] = 'measurement';\nres.payload[\"state_topic\"] = '~/battery_low';\nres.payload[\"unit_of_measurement\"] = '%';\nres.payload[\"value_template\"] = '{{ 0 if value == \"true\" else 100 }}';\nres.payload[\"suggested_display_precision\"] = 0;\n\n// No availability_topic => always available\n\n// Device info\nres.payload[\"device\"] = {\n manufacturer: 'ELV',\n model: 'thermostat',\n name: 'Thermostat ' + msg.payload.room_name + ' / ' + msg.payload.device_name,\n identifiers: ['maxcube_' + msg.payload.rf_address]\n};\n\nreturn res;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 820,
"y": 180,
"wires": [
[
"3c3e46d6fee1be7b"
]
]
},
{
"id": "cf5b45a47dc5be43",
"type": "split",
"z": "103b1fd4c70bd5ad",
"name": "",
"splt": "\\n",
"spltType": "str",
"arraySplt": 1,
"arraySpltType": "len",
"stream": false,
"addname": "",
"x": 650,
"y": 160,
"wires": [
[
"6ad662a6b39283fa",
"794a0609769ac85c"
]
]
},
{
"id": "1e3df1f548625467",
"type": "comment",
"z": "103b1fd4c70bd5ad",
"name": "HA discovery",
"info": "boiler management.\nCompute total of missing degrees in each room and turn on the boiler if needed.",
"x": 310,
"y": 100,
"wires": []
},
{
"id": "b215271d477e76a1",
"type": "function",
"z": "103b1fd4c70bd5ad",
"name": "window",
"func": "var res = {};\nres.topic = 'homeassistant/binary_sensor/' + msg.parts.key.replace(\" \", \"_\") + '/config';\nres.payload = {}\nres.payload[\"~\"] = 'maxcube2mqtt/window/' + msg.payload.room_name.replace(\" \", \"_\");\nres.payload[\"uniq_id\"] = \"maxcube_\" + msg.parts.key.replace(\" \", \"_\");\nres.payload[\"name\"] = \"Contact \" + msg.payload.room_name + \" / \" + msg.payload.device_name;\nres.payload[\"state_topic\"] = \"~/open\";\nres.payload[\"device_class\"] = \"window\";\nres.payload[\"payload_off\"] = \"false\";\nres.payload[\"payload_on\"] = \"true\";\nres.payload[\"availability_topic\"] = \"~/error\";\nres.payload[\"payload_available\"] = \"false\";\nres.payload[\"payload_not_available\"] = \"true\";\nreturn res;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 800,
"y": 220,
"wires": [
[
"3c3e46d6fee1be7b"
]
]
},
{
"id": "b022f80ec1f025d0",
"type": "function",
"z": "103b1fd4c70bd5ad",
"name": "window bat",
"func": "var res = {}\nres.topic = 'homeassistant/sensor/' + msg.parts.key.replace(\" \", \"_\") + \"_battery/config\";\nres.payload = {}\nres.payload[\"~\"] = 'maxcube2mqtt/window/' + msg.payload.room_name.replace(\" \", \"_\");\nres.payload[\"uniq_id\"] = \"maxcube_\" + msg.parts.key.replace(\" \", \"_\") + \"_battery\";\nres.payload[\"name\"] = \"Contact \" + msg.payload.room_name + \" / \" + msg.payload.device_name + \" Battery\";\nres.payload[\"device_class\"] = \"battery\";\nres.payload[\"entity_category\"] = \"diagnostic\";\nres.payload[\"state_class\"] = \"measurement\";\nres.payload[\"state_topic\"] = \"~/battery_low\";\nres.payload[\"unit_of_measurement\"] = \"%\";\nres.payload[\"value_template\"] = '{{ 0 if value == \"true\" else 100 }}';\nreturn res;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 810,
"y": 260,
"wires": [
[
"3c3e46d6fee1be7b"
]
]
},
{
"id": "1e0f3bbea406afcd",
"type": "split",
"z": "103b1fd4c70bd5ad",
"name": "",
"splt": "\\n",
"spltType": "str",
"arraySplt": 1,
"arraySpltType": "len",
"stream": false,
"addname": "",
"x": 650,
"y": 220,
"wires": [
[
"b215271d477e76a1",
"b022f80ec1f025d0"
]
]
},
{
"id": "efcb1347e477233d",
"type": "mqtt out",
"z": "103b1fd4c70bd5ad",
"name": "mqtt",
"topic": "",
"qos": "",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "d7efe4ef4af81eb9",
"x": 1150,
"y": 580,
"wires": []
},
{
"id": "3d3e1fea5d7df796",
"type": "function",
"z": "103b1fd4c70bd5ad",
"name": "decode",
"func": "var res = {};\nfor (const [key, value] of Object.entries(msg.payload)) {\n if (value.device_type) {\n name = value.room_name.replace(\" \", \"_\") + \"_\" + value.device_name.replace(\" \", \"_\");\n if (!(value.device_type in res))\n res[value.device_type] = {};\n res[value.device_type][name] = value;\n }\n}\nreturn [ { \"payload\": res[1] }, { \"payload\": res[3] }, { \"payload\": res[4] }, res ];",
"outputs": 4,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 420,
"y": 780,
"wires": [
[],
[
"f9b774ccf31a5c1f"
],
[],
[]
]
},
{
"id": "dca227a30d266fdf",
"type": "comment",
"z": "103b1fd4c70bd5ad",
"name": "mqtt send",
"info": "send/receive to mqtt",
"x": 420,
"y": 720,
"wires": []
},
{
"id": "f9b774ccf31a5c1f",
"type": "split",
"z": "103b1fd4c70bd5ad",
"name": "",
"splt": "\\n",
"spltType": "str",
"arraySplt": 1,
"arraySpltType": "len",
"stream": false,
"addname": "",
"x": 590,
"y": 780,
"wires": [
[
"8d6bf2e8cf66f9fb"
]
]
},
{
"id": "4d772a73d166d0d8",
"type": "mqtt in",
"z": "103b1fd4c70bd5ad",
"name": "",
"topic": "",
"qos": "2",
"datatype": "auto",
"broker": "d7efe4ef4af81eb9",
"nl": false,
"rap": true,
"rh": 0,
"inputs": 1,
"x": 850,
"y": 780,
"wires": [
[
"b06952c575c8a5a6"
]
]
},
{
"id": "8d6bf2e8cf66f9fb",
"type": "function",
"z": "103b1fd4c70bd5ad",
"name": "subscribe",
"func": "var msgo = {}\nmsgo.action = 'subscribe'\nmsgo.topic = 'maxcube2mqtt/thermostat/' + msg.payload.room_name.replace(\" \", \"_\") + \"/setpoint/set\";\nvar persist = flow.get('persist', 'file');\nif (typeof persist == 'undefined') {\n persist = {};\n}\npersist[msgo.topic] = msg.payload.rf_address;\nflow.set('persist', persist, 'file');\nreturn msgo;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 720,
"y": 780,
"wires": [
[
"4d772a73d166d0d8"
]
]
},
{
"id": "b06952c575c8a5a6",
"type": "function",
"z": "103b1fd4c70bd5ad",
"name": "get_rfaddress",
"func": "var persist = flow.get('persist', 'file');\nreturn { \"payload\": { \"rf_address\": persist[msg.topic], \"degrees\": parseFloat(msg.payload), \"mode\": \"MANUAL\" } };",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1000,
"y": 780,
"wires": [
[
"1b8a19e60d1941d5"
]
]
},
{
"id": "b6d09b6a82b25db2",
"type": "delay",
"z": "103b1fd4c70bd5ad",
"name": "",
"pauseType": "delay",
"timeout": "5",
"timeoutUnits": "seconds",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"allowrate": false,
"outputs": 1,
"x": 1300,
"y": 780,
"wires": [
[
"248a31587c7960fe"
]
]
},
{
"id": "1b8a19e60d1941d5",
"type": "maxcube in",
"z": "103b1fd4c70bd5ad",
"server": "9a07e5adcaeb56f7",
"x": 1160,
"y": 780,
"wires": [
[
"b6d09b6a82b25db2"
]
]
},
{
"id": "7897f3ffd2481dcc",
"type": "inject",
"z": "103b1fd4c70bd5ad",
"name": "on start",
"props": [],
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "1",
"topic": "",
"x": 120,
"y": 780,
"wires": [
[
"d0d37f63eb3c771c"
]
]
},
{
"id": "d0d37f63eb3c771c",
"type": "maxcube out",
"z": "103b1fd4c70bd5ad",
"server": "9a07e5adcaeb56f7",
"singleMessage": true,
"x": 260,
"y": 780,
"wires": [
[
"3d3e1fea5d7df796"
]
]
},
{
"id": "1201580fe85fe424",
"type": "subflow:bae136fa5a77790e",
"z": "103b1fd4c70bd5ad",
"name": "",
"x": 160,
"y": 180,
"wires": [
[
"554700ca0a7b0288"
]
]
},
{
"id": "91688cf578b02dfa",
"type": "function",
"z": "103b1fd4c70bd5ad",
"name": "boiler state timer",
"func": "if (msg.payload == 'ON') {\n msg.payload = 300;\n return msg;\n}\nreturn null;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 780,
"y": 360,
"wires": [
[
"add1600a8c24f3a4"
]
]
},
{
"id": "add1600a8c24f3a4",
"type": "mqtt out",
"z": "103b1fd4c70bd5ad",
"name": "mqtt boiler state",
"topic": "cmnd/tasmota_F4295B/RuleTimer1",
"qos": "0",
"retain": "false",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "d7efe4ef4af81eb9",
"x": 980,
"y": 360,
"wires": []
},
{
"id": "d7efe4ef4af81eb9",
"type": "mqtt-broker",
"name": "MQTT",
"broker": "10.68.69.5",
"port": "1883",
"clientid": "nodered",
"autoConnect": true,
"usetls": false,
"protocolVersion": "4",
"keepalive": "60",
"cleansession": true,
"autoUnsubscribe": true,
"birthTopic": "nodered/status",
"birthQos": "0",
"birthRetain": "true",
"birthPayload": "online",
"birthMsg": {},
"closeTopic": "nodered/status",
"closeQos": "0",
"closeRetain": "true",
"closePayload": "offline",
"closeMsg": {},
"willTopic": "nodered/status",
"willQos": "0",
"willRetain": "true",
"willPayload": "timeout",
"willMsg": {},
"userProps": "",
"sessionExpiry": ""
},
{
"id": "9a07e5adcaeb56f7",
"type": "maxcube-server",
"host": "10.68.69.120",
"port": "62910",
"disabled": false
},
{
"id": "3ef729ba23274c73",
"type": "global-config",
"env": [],
"modules": {
"node-red-contrib-home-assistant-websocket": "0.77.2",
"node-red-contrib-maxcube2": "0.3.5"
}
}
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment