Enhance Node-RED living room flow with new functionality and documentation updates
- Added a new "Living Room" tab to the Node-RED flow for better organization. - Introduced functions for handling Home Assistant and Zigbee interactions, allowing for more dynamic control of living room lights. - Updated the living room flow documentation to include configuration instructions for the Action node, improving usability for users integrating with Home Assistant. This update enhances the functionality and clarity of the Node-RED integration for the living room setup.
This commit is contained in:
@@ -7,6 +7,14 @@
|
||||
"info": "",
|
||||
"env": []
|
||||
},
|
||||
{
|
||||
"id": "7de41d810b04d992",
|
||||
"type": "tab",
|
||||
"label": "Living Room",
|
||||
"disabled": false,
|
||||
"info": "",
|
||||
"env": []
|
||||
},
|
||||
{
|
||||
"id": "480553bb6f5b2b51",
|
||||
"type": "tab",
|
||||
@@ -79,14 +87,6 @@
|
||||
"info": "",
|
||||
"env": []
|
||||
},
|
||||
{
|
||||
"id": "7de41d810b04d992",
|
||||
"type": "tab",
|
||||
"label": "Living Room",
|
||||
"disabled": false,
|
||||
"info": "",
|
||||
"env": []
|
||||
},
|
||||
{
|
||||
"id": "57bd1b149526fcee",
|
||||
"type": "subflow",
|
||||
@@ -4045,7 +4045,7 @@
|
||||
"globaltypes": "",
|
||||
"pack": "true",
|
||||
"port": "",
|
||||
"x": 360,
|
||||
"x": 400,
|
||||
"y": 40,
|
||||
"wires": [
|
||||
[
|
||||
@@ -4084,7 +4084,7 @@
|
||||
"outport": "",
|
||||
"base64": false,
|
||||
"multicast": "false",
|
||||
"x": 580,
|
||||
"x": 620,
|
||||
"y": 40,
|
||||
"wires": []
|
||||
},
|
||||
@@ -4225,7 +4225,8 @@
|
||||
"name": "from zigbee",
|
||||
"links": [
|
||||
"629cbd7ce15bb9e3",
|
||||
"6fb9cc41c7b14fac"
|
||||
"6fb9cc41c7b14fac",
|
||||
"74c2640db1c8bd3e"
|
||||
],
|
||||
"x": 245,
|
||||
"y": 100,
|
||||
@@ -5034,10 +5035,11 @@
|
||||
"3b5e1f4f9ccd9c74",
|
||||
"2b29c71c180a50df",
|
||||
"860779317fad91b5",
|
||||
"8dd17125d6bb49f7"
|
||||
"8dd17125d6bb49f7",
|
||||
"7c4268195123e319"
|
||||
],
|
||||
"x": 1175,
|
||||
"y": 80,
|
||||
"x": 1205,
|
||||
"y": 100,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
@@ -6101,7 +6103,7 @@
|
||||
"type": "debug",
|
||||
"z": "46892ced77481340",
|
||||
"name": "debug 27",
|
||||
"active": true,
|
||||
"active": false,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
@@ -6280,7 +6282,7 @@
|
||||
"z": "46892ced77481340",
|
||||
"name": "to UDP",
|
||||
"links": [],
|
||||
"x": 375,
|
||||
"x": 285,
|
||||
"y": 20,
|
||||
"wires": [
|
||||
[
|
||||
@@ -6288,6 +6290,273 @@
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "4045239f675b77d2",
|
||||
"type": "function",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "HA to NVL",
|
||||
"func": "// Writes to state.rooms.cmd_livingroom (NVL variable cmd_livingroom).\nconst ROOM_NAME = 'cmd_livingroom';\nconst entityId = (msg.topic || '').toString();\nconst parts = entityId.split('_');\nconst last = parts[parts.length - 1] || '';\nconst num = parseInt(last, 10);\nconst lightNum = (!isNaN(num) && num >= 1) ? Math.min(6, num) : 1;\n\n// Normalize state: HA / trigger-state can send payload as string, object with .state, or data.new_state.state\nlet rawState = msg.payload;\nif (rawState !== null && typeof rawState === 'object' && rawState.state !== undefined) rawState = rawState.state;\nif (msg.data && msg.data.new_state && msg.data.new_state.state !== undefined) rawState = msg.data.new_state.state;\nconst isOn = (rawState === 'on' || rawState === true);\n\nnode.warn('[HA to NVL] topic=' + entityId + ' rawState=' + JSON.stringify(rawState) + ' → lightNum=' + lightNum + ' isOn=' + isOn);\n\nif (!flow.get('nvlInState')) flow.set('nvlInState', { rooms: {}, boiler: {} });\nconst state = flow.get('nvlInState');\nif (!state.rooms[ROOM_NAME]) state.rooms[ROOM_NAME] = {};\nconst r = state.rooms[ROOM_NAME];\nr['ha_l' + lightNum + '_on'] = false;\nr['ha_l' + lightNum + '_off'] = false;\nif (isOn) r['ha_l' + lightNum + '_on'] = true;\nelse r['ha_l' + lightNum + '_off'] = true;\nflow.set('nvlInState', state);\n\nnode.warn('[HA to NVL] set cmd_livingroom ha_l' + lightNum + '_' + (isOn ? 'on' : 'off') + '=true, buildAndSend');\n\n// PLC uses R_TRIG (rising edge). If \"off\" still doesn’t work, add a flow: after this node, 80ms delay then clear ha_l*_off and buildAndSend again (pulse).\nmsg.payload = { buildAndSend: true };\nreturn msg;\n",
|
||||
"outputs": 1,
|
||||
"timeout": "",
|
||||
"noerr": 0,
|
||||
"initialize": "",
|
||||
"finalize": "",
|
||||
"libs": [],
|
||||
"x": 1170,
|
||||
"y": 320,
|
||||
"wires": [
|
||||
[
|
||||
"75212a4bc3d085db"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "718ba68ae647874f",
|
||||
"type": "function",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "Zigbee to NVL",
|
||||
"func": "// Writes to state.rooms.cmd_livingroom (same as HA to NVL). Zigbee action → zigbee_sw1..6.\nconst ROOM_NAME = 'cmd_livingroom';\nconst actionToSwitch = { single: 1, double: 2, hold: 3, release: 4, triple: 5, quad: 6 };\nconst payload = msg.payload || {};\nconst actionRaw = (payload.action || payload.click || '').toLowerCase();\n// Support \"1_single\", \"2_double\" etc. (button_number + action) or plain \"single\", \"double\"\nconst parts = actionRaw.split('_');\nlet swIndex = null;\nif (parts.length >= 2) {\n const buttonNum = parseInt(parts[0], 10);\n if (buttonNum >= 1 && buttonNum <= 6) swIndex = buttonNum;\n}\nif (swIndex == null) swIndex = actionToSwitch[actionRaw];\nif (swIndex == null && parts.length >= 2) swIndex = actionToSwitch[parts.slice(1).join('_')];\nif (swIndex == null) swIndex = actionToSwitch[parts[parts.length - 1]];\n\nif (swIndex == null) {\n node.warn('[Zigbee to NVL] unknown action: ' + JSON.stringify(actionRaw) + ' payload=' + JSON.stringify(payload));\n return null;\n}\n\nif (!flow.get('nvlInState')) flow.set('nvlInState', { rooms: {}, boiler: {} });\nconst state = flow.get('nvlInState');\nif (!state.rooms[ROOM_NAME]) state.rooms[ROOM_NAME] = {};\nstate.rooms[ROOM_NAME]['zigbee_sw' + swIndex] = true;\nflow.set('nvlInState', state);\n\nnode.warn('[Zigbee to NVL] cmd_livingroom zigbee_sw' + swIndex + '=true (action=' + actionRaw + '), buildAndSend + zigbeeClear');\n\nmsg.payload = { buildAndSend: true };\nmsg.zigbeeClear = { room: ROOM_NAME, key: 'zigbee_sw' + swIndex };\nreturn msg;\n",
|
||||
"outputs": 1,
|
||||
"timeout": "",
|
||||
"noerr": 0,
|
||||
"initialize": "",
|
||||
"finalize": "",
|
||||
"libs": [],
|
||||
"x": 1160,
|
||||
"y": 380,
|
||||
"wires": [
|
||||
[
|
||||
"886c1334e909e2cd",
|
||||
"75212a4bc3d085db"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "886c1334e909e2cd",
|
||||
"type": "delay",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "80ms",
|
||||
"pauseType": "delay",
|
||||
"timeout": "80",
|
||||
"timeoutUnits": "milliseconds",
|
||||
"rate": "1",
|
||||
"nbRateUnits": "1",
|
||||
"rateUnits": "seconds",
|
||||
"randomFirst": "1",
|
||||
"randomLast": "5",
|
||||
"randomUnits": "seconds",
|
||||
"drop": true,
|
||||
"outputs": 1,
|
||||
"x": 1350,
|
||||
"y": 380,
|
||||
"wires": [
|
||||
[
|
||||
"44822e048dbaee31"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "44822e048dbaee31",
|
||||
"type": "function",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "Clear zigbee edge",
|
||||
"func": "if (msg.zigbeeClear) {\n const state = flow.get('nvlInState') || { rooms: {}, boiler: {} };\n const r = state.rooms[msg.zigbeeClear.room];\n if (r) r[msg.zigbeeClear.key] = false;\n flow.set('nvlInState', state);\n}\nmsg.payload = { buildAndSend: true };\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"x": 1550,
|
||||
"y": 380,
|
||||
"wires": [
|
||||
[
|
||||
"75212a4bc3d085db"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "17814d6d1beda546",
|
||||
"type": "zigbee2mqtt-in",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "",
|
||||
"server": "4e20fc347c658518",
|
||||
"friendly_name": "Office Switch (TS0042)",
|
||||
"device_id": "0xa4c138a5b9771b05",
|
||||
"state": "0",
|
||||
"outputAtStartup": true,
|
||||
"filterChanges": false,
|
||||
"enableMultiple": false,
|
||||
"x": 940,
|
||||
"y": 380,
|
||||
"wires": [
|
||||
[
|
||||
"718ba68ae647874f"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "74c2640db1c8bd3e",
|
||||
"type": "link out",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "link out 26",
|
||||
"mode": "link",
|
||||
"links": [
|
||||
"c6162cb59e21491f"
|
||||
],
|
||||
"x": 1570,
|
||||
"y": 320,
|
||||
"wires": [],
|
||||
"l": true
|
||||
},
|
||||
{
|
||||
"id": "95b626ea6980446e",
|
||||
"type": "trigger-state",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "",
|
||||
"server": "29eb84a2708d96e0",
|
||||
"version": 5,
|
||||
"inputs": 1,
|
||||
"outputs": 2,
|
||||
"exposeAsEntityConfig": "",
|
||||
"entities": {
|
||||
"entity": [
|
||||
"input_boolean.living_room_new"
|
||||
],
|
||||
"substring": [],
|
||||
"regex": []
|
||||
},
|
||||
"debugEnabled": true,
|
||||
"constraints": [],
|
||||
"customOutputs": [],
|
||||
"outputInitially": false,
|
||||
"stateType": "str",
|
||||
"enableInput": true,
|
||||
"x": 890,
|
||||
"y": 320,
|
||||
"wires": [
|
||||
[
|
||||
"4045239f675b77d2"
|
||||
],
|
||||
[]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "75212a4bc3d085db",
|
||||
"type": "function",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "NVL SEND",
|
||||
"func": "// Build the payload for nvl-send from flow.nvlInState.\n// Redesign: all rooms use struct_room_cmds. Add more names to roomNames when you add rooms.\n// Wire: HA to NVL / Zigbee to NVL → this function → nvl-send → udp out\n\nconst struct_room_cmds_default = {\n ha_l1_on: false, ha_l1_off: false, ha_l2_on: false, ha_l2_off: false,\n ha_l3_on: false, ha_l3_off: false, ha_l4_on: false, ha_l4_off: false,\n ha_l5_on: false, ha_l5_off: false, ha_l6_on: false, ha_l6_off: false,\n zigbee_sw1: false, zigbee_sw2: false, zigbee_sw3: false,\n zigbee_sw4: false, zigbee_sw5: false, zigbee_sw6: false,\n ha_all_on: false, ha_all_off: false\n};\n\n// NVL variable names per room (struct_room_cmds). Add more when you add rooms.\nconst roomNames = ['cmd_livingroom'];\n\nconst state = flow.get('nvlInState') || { rooms: {}, boiler: {} };\nconst rooms = state.rooms || {};\nconst payload = {};\n\nfor (const name of roomNames) {\n const cmd = rooms[name] || {};\n payload[name] = { ...struct_room_cmds_default };\n for (const k of Object.keys(struct_room_cmds_default)) {\n if (cmd[k] !== undefined) payload[name][k] = !!cmd[k];\n }\n}\n\nmsg.payload = payload;\nreturn msg;\n",
|
||||
"outputs": 1,
|
||||
"timeout": "",
|
||||
"noerr": 0,
|
||||
"initialize": "",
|
||||
"finalize": "",
|
||||
"libs": [],
|
||||
"x": 1370,
|
||||
"y": 320,
|
||||
"wires": [
|
||||
[
|
||||
"74c2640db1c8bd3e"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "d5cdc38171230744",
|
||||
"type": "function",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "NVL to HA Sync",
|
||||
"func": "// Run this AFTER nvl-receive. Syncs all mapped lights from PLC state to HA entities.\n// Connect output 1 → api-call-service (input_boolean.turn_on / light.turn_on), output 2 → api-call-service (turn_off).\n// Only outputs when state changed (per entity). Add more entries to LIGHT_ENTITY_MAP as you add rooms/lights.\n\n// Map: NVL room key (in payload) + light index 1..6 → HA entity_id\nconst LIGHT_ENTITY_MAP = [\n { room: 'light_livingRoom', light: 1, entityId: 'input_boolean.living_room_new' },\n // { room: 'light_livingRoom', light: 2, entityId: 'input_boolean.living_room_2' },\n // { room: 'l_kitchen', light: 1, entityId: 'light.kitchen_ceiling' },\n];\n\nconst payload = msg.payload || {};\nconst onMsgs = [];\nconst offMsgs = [];\n\nfor (const entry of LIGHT_ENTITY_MAP) {\n const room = payload[entry.room] || {};\n const isOn = !!(room['l_' + entry.light] || room['l' + entry.light]);\n\n const flowKey = 'nvlToHa_' + entry.entityId.replace(/\\./g, '_');\n const last = flow.get(flowKey);\n if (last === isOn) continue;\n flow.set(flowKey, isOn);\n\n const out = { payload: { entity_id: entry.entityId } };\n if (isOn) onMsgs.push(out); else offMsgs.push(out);\n}\n\nif (onMsgs.length === 0 && offMsgs.length === 0) return null;\nreturn [onMsgs, offMsgs];\n",
|
||||
"outputs": 2,
|
||||
"timeout": 0,
|
||||
"noerr": 0,
|
||||
"initialize": "",
|
||||
"finalize": "",
|
||||
"libs": [],
|
||||
"x": 1240,
|
||||
"y": 220,
|
||||
"wires": [
|
||||
[
|
||||
"566f142b2e6e841f"
|
||||
],
|
||||
[
|
||||
"5789f9ce23be8782"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "7c4268195123e319",
|
||||
"type": "link in",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "link in 14",
|
||||
"links": [
|
||||
"e059a9fd1da3c3fd"
|
||||
],
|
||||
"x": 1015,
|
||||
"y": 220,
|
||||
"wires": [
|
||||
[
|
||||
"d5cdc38171230744"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "566f142b2e6e841f",
|
||||
"type": "api-call-service",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "",
|
||||
"server": "29eb84a2708d96e0",
|
||||
"version": 7,
|
||||
"debugenabled": false,
|
||||
"action": "input_boolean.turn_on",
|
||||
"floorId": [],
|
||||
"areaId": [],
|
||||
"deviceId": [],
|
||||
"entityId": [
|
||||
"input_boolean.living_room_new"
|
||||
],
|
||||
"labelId": [],
|
||||
"data": "",
|
||||
"dataType": "jsonata",
|
||||
"mergeContext": "",
|
||||
"mustacheAltTags": false,
|
||||
"outputProperties": [],
|
||||
"queue": "none",
|
||||
"blockInputOverrides": false,
|
||||
"domain": "input_boolean",
|
||||
"service": "turn_on",
|
||||
"x": 1540,
|
||||
"y": 200,
|
||||
"wires": [
|
||||
[]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "5789f9ce23be8782",
|
||||
"type": "api-call-service",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "",
|
||||
"server": "29eb84a2708d96e0",
|
||||
"version": 7,
|
||||
"debugenabled": false,
|
||||
"action": "input_boolean.turn_off",
|
||||
"floorId": [],
|
||||
"areaId": [],
|
||||
"deviceId": [],
|
||||
"entityId": [
|
||||
"input_boolean.living_room_new"
|
||||
],
|
||||
"labelId": [],
|
||||
"data": "",
|
||||
"dataType": "jsonata",
|
||||
"mergeContext": "",
|
||||
"mustacheAltTags": false,
|
||||
"outputProperties": [],
|
||||
"queue": "none",
|
||||
"blockInputOverrides": false,
|
||||
"domain": "input_boolean",
|
||||
"service": "turn_off",
|
||||
"x": 1540,
|
||||
"y": 260,
|
||||
"wires": [
|
||||
[]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "d592161c8e50638a",
|
||||
"type": "debug",
|
||||
@@ -14067,7 +14336,7 @@
|
||||
"type": "function",
|
||||
"z": "24e9a56732cfa6f1",
|
||||
"name": "HA to NVL",
|
||||
"func": "const ROOM_NAME = 'cmd_livingroom';\nconst entityId = (msg.topic || '').toString();\nconst parts = entityId.split('_');\nconst last = parts[parts.length - 1] || '';\nconst num = parseInt(last, 10);\nconst lightNum = (!isNaN(num) && num >= 1) ? Math.min(6, num) : 1;\nconst isOn = (msg.payload === 'on' || msg.payload === true);\nnode.warn('[HA to NVL] topic=' + entityId + ' payload=' + JSON.stringify(msg.payload) + ' (type ' + typeof msg.payload + ') → lightNum=' + lightNum + ' isOn=' + isOn);\nif (!flow.get('nvlInState')) flow.set('nvlInState', { rooms: {}, boiler: {} });\nconst state = flow.get('nvlInState');\nif (!state.rooms[ROOM_NAME]) state.rooms[ROOM_NAME] = {};\nconst r = state.rooms[ROOM_NAME];\nr['ha_l' + lightNum + '_on'] = false;\nr['ha_l' + lightNum + '_off'] = false;\nif (isOn) r['ha_l' + lightNum + '_on'] = true;\nelse r['ha_l' + lightNum + '_off'] = true;\nflow.set('nvlInState', state);\nnode.warn('[HA to NVL] set cmd_livingroom ha_l' + lightNum + '_' + (isOn ? 'on' : 'off') + '=true, buildAndSend');\nmsg.payload = { buildAndSend: true };\nreturn msg;",
|
||||
"func": "const ROOM_NAME = 'livingRoom';\nconst entityId = (msg.topic || '').toString();\nconst parts = entityId.split('_');\nconst last = parts[parts.length - 1] || '';\nconst num = parseInt(last, 10);\nconst lightNum = (!isNaN(num) && num >= 1) ? Math.min(6, num) : 1;\nconst isOn = (msg.payload === 'on' || msg.payload === true);\nif (!flow.get('nvlInState')) flow.set('nvlInState', { rooms: {}, boiler: {} });\nconst state = flow.get('nvlInState');\nif (!state.rooms[ROOM_NAME]) state.rooms[ROOM_NAME] = {};\nconst r = state.rooms[ROOM_NAME];\nr['ha_l' + lightNum + '_on'] = false;\nr['ha_l' + lightNum + '_off'] = false;\nif (isOn) r['ha_l' + lightNum + '_on'] = true;\nelse r['ha_l' + lightNum + '_off'] = true;\nflow.set('nvlInState', state);\nmsg.payload = { buildAndSend: true };\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"x": 540,
|
||||
"y": 220,
|
||||
@@ -14148,7 +14417,7 @@
|
||||
"type": "function",
|
||||
"z": "24e9a56732cfa6f1",
|
||||
"name": "Build NVL_In",
|
||||
"func": "const roomSize = 20;\nconst roomNames = ['masterBedroom','masterBathroom','bedroom_1','bedroom_2','bathroom','guestWc','kitchen','pantry','cmd_livingroom','dinningRoom','entrance','hallway','outVeranda','outFront','outBack','outSide'];\nconst state = flow.get('nvlInState') || { rooms: {}, boiler: {} };\nconst rooms = state.rooms || {};\nconst boiler = state.boiler || {};\nconst buf = Buffer.alloc(328);\nfor (let r = 0; r < 16; r++) {\n const c = rooms[roomNames[r]] || {};\n const base = r * roomSize;\n buf[base+0]=c.ha_l1_on?1:0;buf[base+1]=c.ha_l1_off?1:0;buf[base+2]=c.ha_l2_on?1:0;buf[base+3]=c.ha_l2_off?1:0;buf[base+4]=c.ha_l3_on?1:0;buf[base+5]=c.ha_l3_off?1:0;buf[base+6]=c.ha_l4_on?1:0;buf[base+7]=c.ha_l4_off?1:0;buf[base+8]=c.ha_l5_on?1:0;buf[base+9]=c.ha_l5_off?1:0;buf[base+10]=c.ha_l6_on?1:0;buf[base+11]=c.ha_l6_off?1:0;\n for (let i = 0; i < 6; i++) buf[base+12+i] = c['zigbee_sw'+(i+1)] ? 1 : 0;\n buf[base+18]=c.ha_all_on?1:0;buf[base+19]=c.ha_all_off?1:0;\n}\nconst bo = 320;\nbuf[bo+0]=boiler.ha_on?1:0;buf[bo+1]=boiler.ha_off?1:0;buf[bo+2]=boiler.schedule_on?1:0;buf[bo+3]=boiler.schedule_off?1:0;buf[bo+4]=boiler.emergency_stop?1:0;buf.writeInt16LE(boiler.max_on_time_minutes??480,bo+6);\nmsg.payload = buf;\nreturn msg;",
|
||||
"func": "const roomSize = 20;\nconst roomNames = ['masterBedroom','masterBathroom','bedroom_1','bedroom_2','bathroom','guestWc','kitchen','pantry','livingRoom','dinningRoom','entrance','hallway','outVeranda','outFront','outBack','outSide'];\nconst state = flow.get('nvlInState') || { rooms: {}, boiler: {} };\nconst rooms = state.rooms || {};\nconst boiler = state.boiler || {};\nconst buf = Buffer.alloc(328);\nfor (let r = 0; r < 16; r++) {\n const c = rooms[roomNames[r]] || {};\n const base = r * roomSize;\n buf[base+0]=c.ha_l1_on?1:0;buf[base+1]=c.ha_l1_off?1:0;buf[base+2]=c.ha_l2_on?1:0;buf[base+3]=c.ha_l2_off?1:0;buf[base+4]=c.ha_l3_on?1:0;buf[base+5]=c.ha_l3_off?1:0;buf[base+6]=c.ha_l4_on?1:0;buf[base+7]=c.ha_l4_off?1:0;buf[base+8]=c.ha_l5_on?1:0;buf[base+9]=c.ha_l5_off?1:0;buf[base+10]=c.ha_l6_on?1:0;buf[base+11]=c.ha_l6_off?1:0;\n for (let i = 0; i < 6; i++) buf[base+12+i] = c['zigbee_sw'+(i+1)] ? 1 : 0;\n buf[base+18]=c.ha_all_on?1:0;buf[base+19]=c.ha_all_off?1:0;\n}\nconst bo = 320;\nbuf[bo+0]=boiler.ha_on?1:0;buf[bo+1]=boiler.ha_off?1:0;buf[bo+2]=boiler.schedule_on?1:0;buf[bo+3]=boiler.schedule_off?1:0;buf[bo+4]=boiler.emergency_stop?1:0;buf.writeInt16LE(boiler.max_on_time_minutes??480,bo+6);\nmsg.payload = buf;\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"timeout": "",
|
||||
"noerr": 0,
|
||||
@@ -14237,268 +14506,5 @@
|
||||
"wires": [
|
||||
[]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "4045239f675b77d2",
|
||||
"type": "function",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "HA to NVL",
|
||||
"func": "const ROOM_NAME = 'cmd_livingroom';\nconst entityId = (msg.topic || '').toString();\nconst parts = entityId.split('_');\nconst last = parts[parts.length - 1] || '';\nconst num = parseInt(last, 10);\nconst lightNum = (!isNaN(num) && num >= 1) ? Math.min(6, num) : 1;\nconst isOn = (msg.payload === 'on' || msg.payload === true);\nnode.warn('[HA to NVL] topic=' + entityId + ' payload=' + JSON.stringify(msg.payload) + ' (type ' + typeof msg.payload + ') → lightNum=' + lightNum + ' isOn=' + isOn);\nif (!flow.get('nvlInState')) flow.set('nvlInState', { rooms: {}, boiler: {} });\nconst state = flow.get('nvlInState');\nif (!state.rooms[ROOM_NAME]) state.rooms[ROOM_NAME] = {};\nconst r = state.rooms[ROOM_NAME];\nr['ha_l' + lightNum + '_on'] = false;\nr['ha_l' + lightNum + '_off'] = false;\nif (isOn) r['ha_l' + lightNum + '_on'] = true;\nelse r['ha_l' + lightNum + '_off'] = true;\nflow.set('nvlInState', state);\nnode.warn('[HA to NVL] set cmd_livingroom ha_l' + lightNum + '_' + (isOn ? 'on' : 'off') + '=true, buildAndSend');\nmsg.payload = { buildAndSend: true };\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"x": 1170,
|
||||
"y": 320,
|
||||
"wires": [
|
||||
[
|
||||
"75212a4bc3d085db",
|
||||
"f891d88454556409"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "718ba68ae647874f",
|
||||
"type": "function",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "Zigbee to NVL",
|
||||
"func": "const ROOM_NAME = 'livingRoom';\nconst actionToSwitch = { single: 1, double: 2, hold: 3, release: 4, triple: 5, quad: 6 };\nconst payload = msg.payload || {};\nconst action = (payload.action || payload.click || '').toLowerCase();\nconst swIndex = actionToSwitch[action];\nif (swIndex == null) return null;\nif (!flow.get('nvlInState')) flow.set('nvlInState', { rooms: {}, boiler: {} });\nconst state = flow.get('nvlInState');\nif (!state.rooms[ROOM_NAME]) state.rooms[ROOM_NAME] = {};\nstate.rooms[ROOM_NAME]['zigbee_sw' + swIndex] = true;\nflow.set('nvlInState', state);\nmsg.payload = { buildAndSend: true };\nmsg.zigbeeClear = { room: ROOM_NAME, key: 'zigbee_sw' + swIndex };\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"x": 1120,
|
||||
"y": 420,
|
||||
"wires": [
|
||||
[
|
||||
"75212a4bc3d085db",
|
||||
"886c1334e909e2cd",
|
||||
"c0ba289781c06018"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "886c1334e909e2cd",
|
||||
"type": "delay",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "80ms",
|
||||
"pauseType": "delay",
|
||||
"timeout": "80",
|
||||
"timeoutUnits": "milliseconds",
|
||||
"rate": "1",
|
||||
"nbRateUnits": "1",
|
||||
"rateUnits": "seconds",
|
||||
"randomFirst": "1",
|
||||
"randomLast": "5",
|
||||
"randomUnits": "seconds",
|
||||
"drop": true,
|
||||
"outputs": 1,
|
||||
"x": 1340,
|
||||
"y": 420,
|
||||
"wires": [
|
||||
[
|
||||
"44822e048dbaee31"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "44822e048dbaee31",
|
||||
"type": "function",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "Clear zigbee edge",
|
||||
"func": "if (msg.zigbeeClear) {\n const state = flow.get('nvlInState') || { rooms: {}, boiler: {} };\n const r = state.rooms[msg.zigbeeClear.room];\n if (r) r[msg.zigbeeClear.key] = false;\n flow.set('nvlInState', state);\n}\nmsg.payload = { buildAndSend: true };\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"x": 1550,
|
||||
"y": 420,
|
||||
"wires": [
|
||||
[
|
||||
"75212a4bc3d085db",
|
||||
"b9b1a23de2bb693b"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "75212a4bc3d085db",
|
||||
"type": "function",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "Build NVL_In",
|
||||
"func": "const roomSize = 20;\nconst roomNames = ['masterBedroom','masterBathroom','bedroom_1','bedroom_2','bathroom','guestWc','kitchen','pantry','cmd_livingroom','dinningRoom','entrance','hallway','outVeranda','outFront','outBack','outSide'];\nconst state = flow.get('nvlInState') || { rooms: {}, boiler: {} };\nconst rooms = state.rooms || {};\nconst boiler = state.boiler || {};\nconst buf = Buffer.alloc(328);\nfor (let r = 0; r < 16; r++) {\n const c = rooms[roomNames[r]] || {};\n const base = r * roomSize;\n buf[base+0]=c.ha_l1_on?1:0;buf[base+1]=c.ha_l1_off?1:0;buf[base+2]=c.ha_l2_on?1:0;buf[base+3]=c.ha_l2_off?1:0;buf[base+4]=c.ha_l3_on?1:0;buf[base+5]=c.ha_l3_off?1:0;buf[base+6]=c.ha_l4_on?1:0;buf[base+7]=c.ha_l4_off?1:0;buf[base+8]=c.ha_l5_on?1:0;buf[base+9]=c.ha_l5_off?1:0;buf[base+10]=c.ha_l6_on?1:0;buf[base+11]=c.ha_l6_off?1:0;\n for (let i = 0; i < 6; i++) buf[base+12+i] = c['zigbee_sw'+(i+1)] ? 1 : 0;\n buf[base+18]=c.ha_all_on?1:0;buf[base+19]=c.ha_all_off?1:0;\n}\nconst bo = 320;\nbuf[bo+0]=boiler.ha_on?1:0;buf[bo+1]=boiler.ha_off?1:0;buf[bo+2]=boiler.schedule_on?1:0;buf[bo+3]=boiler.schedule_off?1:0;buf[bo+4]=boiler.emergency_stop?1:0;buf.writeInt16LE(boiler.max_on_time_minutes??480,bo+6);\nmsg.payload = buf;\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"x": 1360,
|
||||
"y": 360,
|
||||
"wires": [
|
||||
[
|
||||
"74c2640db1c8bd3e",
|
||||
"9e9d60859e34c65e"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "17814d6d1beda546",
|
||||
"type": "zigbee2mqtt-in",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "",
|
||||
"server": "4e20fc347c658518",
|
||||
"friendly_name": "Living Room Door Switch (TS0044)",
|
||||
"device_id": "0xa4c1383d7921827a",
|
||||
"state": "0",
|
||||
"outputAtStartup": true,
|
||||
"filterChanges": false,
|
||||
"enableMultiple": false,
|
||||
"x": 820,
|
||||
"y": 420,
|
||||
"wires": [
|
||||
[
|
||||
"718ba68ae647874f"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "74c2640db1c8bd3e",
|
||||
"type": "link out",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "link out 26",
|
||||
"mode": "link",
|
||||
"links": [],
|
||||
"x": 1620,
|
||||
"y": 360,
|
||||
"wires": [],
|
||||
"l": true
|
||||
},
|
||||
{
|
||||
"id": "95b626ea6980446e",
|
||||
"type": "trigger-state",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "",
|
||||
"server": "29eb84a2708d96e0",
|
||||
"version": 5,
|
||||
"inputs": 1,
|
||||
"outputs": 2,
|
||||
"exposeAsEntityConfig": "",
|
||||
"entities": {
|
||||
"entity": [
|
||||
"input_boolean.living_room_new"
|
||||
],
|
||||
"substring": [],
|
||||
"regex": []
|
||||
},
|
||||
"debugEnabled": true,
|
||||
"constraints": [],
|
||||
"customOutputs": [],
|
||||
"outputInitially": false,
|
||||
"stateType": "str",
|
||||
"enableInput": true,
|
||||
"x": 890,
|
||||
"y": 320,
|
||||
"wires": [
|
||||
[
|
||||
"4045239f675b77d2",
|
||||
"b72750450ceb1acc"
|
||||
],
|
||||
[]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "745066d593433c28",
|
||||
"type": "inject",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "",
|
||||
"props": [
|
||||
{
|
||||
"p": "payload"
|
||||
},
|
||||
{
|
||||
"p": "topic",
|
||||
"vt": "str"
|
||||
}
|
||||
],
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": false,
|
||||
"onceDelay": 0.1,
|
||||
"topic": "",
|
||||
"payload": "",
|
||||
"payloadType": "date",
|
||||
"x": 620,
|
||||
"y": 320,
|
||||
"wires": [
|
||||
[
|
||||
"95b626ea6980446e"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "b72750450ceb1acc",
|
||||
"type": "debug",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "debug 36",
|
||||
"active": false,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "false",
|
||||
"statusVal": "",
|
||||
"statusType": "auto",
|
||||
"x": 1130,
|
||||
"y": 260,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "f891d88454556409",
|
||||
"type": "debug",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "debug 37",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "false",
|
||||
"statusVal": "",
|
||||
"statusType": "auto",
|
||||
"x": 1380,
|
||||
"y": 260,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "9e9d60859e34c65e",
|
||||
"type": "debug",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "debug 39",
|
||||
"active": false,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "false",
|
||||
"statusVal": "",
|
||||
"statusType": "auto",
|
||||
"x": 1560,
|
||||
"y": 300,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "c0ba289781c06018",
|
||||
"type": "debug",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "debug 40",
|
||||
"active": false,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "false",
|
||||
"statusVal": "",
|
||||
"statusType": "auto",
|
||||
"x": 1230,
|
||||
"y": 500,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "b9b1a23de2bb693b",
|
||||
"type": "debug",
|
||||
"z": "7de41d810b04d992",
|
||||
"name": "debug 41",
|
||||
"active": false,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "false",
|
||||
"statusVal": "",
|
||||
"statusType": "auto",
|
||||
"x": 1690,
|
||||
"y": 460,
|
||||
"wires": []
|
||||
}
|
||||
]
|
||||
89
docs/integration/nodered-livingroom-analysis.md
Normal file
89
docs/integration/nodered-livingroom-analysis.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Living Room flow – current analysis
|
||||
|
||||
Analysis of the **Living Room** tab as pulled from Node-RED (root@10.20.30.12) on the current flows.
|
||||
|
||||
---
|
||||
|
||||
## Tab
|
||||
|
||||
| Property | Value |
|
||||
|----------|--------|
|
||||
| **Tab id** | `7de41d810b04d992` |
|
||||
| **Label** | Living Room |
|
||||
| **Nodes** | 12 |
|
||||
|
||||
---
|
||||
|
||||
## Nodes (in flow order)
|
||||
|
||||
### 1. Inputs
|
||||
|
||||
| Node | Type | Config | Out →
|
||||
|------|------|--------|------
|
||||
| **(unnamed)** | **trigger-state** | Server: `29eb84a2708d96e0`. Entity: **`input_boolean.living_room_new`**. 2 outputs (allowed, blocked). | → HA to NVL |
|
||||
| **(unnamed)** | **zigbee2mqtt-in** | Server: `4e20fc347c658518`. **Friendly name: Office Switch (TS0042)**. | → Zigbee to NVL |
|
||||
|
||||
### 2. HA → NVL path
|
||||
|
||||
| Node | Type | Role | Out →
|
||||
|------|------|------|------
|
||||
| **HA to NVL** | function | ROOM_NAME = `cmd_livingroom`. Reads `msg.topic` / payload, sets `ha_lN_on` / `ha_lN_off` in `flow.nvlInState.rooms.cmd_livingroom`, outputs `buildAndSend`. | → NVL SEND |
|
||||
|
||||
### 3. Zigbee → NVL path
|
||||
|
||||
| Node | Type | Role | Out →
|
||||
|------|------|------|------
|
||||
| **Zigbee to NVL** | function | ROOM_NAME = `cmd_livingroom`. Maps action (e.g. `1_single`) to `zigbee_swN`, sets state, `zigbeeClear`, `buildAndSend`. | → 80ms, NVL SEND |
|
||||
| **80ms** | delay | 80 ms. | → Clear zigbee edge |
|
||||
| **Clear zigbee edge** | function | Clears `zigbee_swN` in state, sets `buildAndSend`. | → NVL SEND |
|
||||
|
||||
### 4. NVL send path
|
||||
|
||||
| Node | Type | Role | Out →
|
||||
|------|------|------|------
|
||||
| **NVL SEND** | function | Builds payload from `flow.nvlInState` (roomNames with `cmd_livingroom`), struct_room_cmds. | → link out 26 |
|
||||
|
||||
**link out 26** → (cross-tab) → Flow 1 **to UDP** → **udp out** (PLC).
|
||||
|
||||
### 5. PLC → HA sync path (in Living Room tab)
|
||||
|
||||
| Node | Type | Role | Out →
|
||||
|------|------|------|------
|
||||
| **link in 14** | link in | Receives from **Flow 1** “from PLC states” link out (after nvl-receive). | → NVL to HA Sync |
|
||||
| **NVL to HA Sync** | function | Compares PLC light state to last; outputs only on change. Output 1 = turn_on msgs, Output 2 = turn_off msgs (`payload.action` + `payload.target.entity_id`). | → two api-call-service nodes |
|
||||
| **(unnamed)** | **api-call-service** | Action: **`input_boolean.turn_on`**. Entity: **`input_boolean.living_room_new`**. | — |
|
||||
| **(unnamed)** | **api-call-service** | Action: **`input_boolean.turn_off`**. Entity: **`input_boolean.living_room_new`**. | — |
|
||||
|
||||
---
|
||||
|
||||
## Flow diagram (current)
|
||||
|
||||
```
|
||||
[trigger-state] [zigbee2mqtt-in]
|
||||
input_boolean.living_room_new Office Switch (TS0042)
|
||||
| |
|
||||
v v
|
||||
HA to NVL <------------------------ Zigbee to NVL
|
||||
| |
|
||||
| +---> 80ms --> Clear zigbee edge
|
||||
| | |
|
||||
+------------------------------------+--------------+
|
||||
|
|
||||
v
|
||||
NVL SEND --> link out 26 --------> (Flow 1: to UDP --> PLC)
|
||||
|
||||
(Flow 1: udp in --> nvl-receive --> "from PLC states" link out)
|
||||
|
|
||||
v
|
||||
link in 14 --> NVL to HA Sync --> Output 1 --> api-call-service (turn_on)
|
||||
--> Output 2 --> api-call-service (turn_off)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
1. **Zigbee device** in this tab is **Office Switch (TS0042)**; commands still go to **cmd_livingroom** (living room NVL). If the switch is physically in the office, you may want a separate room/flow or map it to another room in Zigbee to NVL.
|
||||
2. **NVL SEND** matches the redesign: single list of rooms (`roomNames`), only **cmd_livingroom** for now; link out 26 sends to Flow 1 → UDP.
|
||||
3. **PLC → HA** sync is in the Living Room tab: **link in 14** gets PLC state from Flow 1, **NVL to HA Sync** outputs only on change, two **api-call-service** nodes with fixed **`input_boolean.turn_on`** / **`input_boolean.turn_off`** and **`input_boolean.living_room_new`**. With “Block Input Overrides” off, the sync’s `msg.payload` would override; if it’s on, the fixed entity is used (which matches the single current entity).
|
||||
4. **Flows file** in the repo has been updated from this pull: `docs/integration/nodered-flows.json`.
|
||||
@@ -151,6 +151,19 @@ When you change a light with the **Zigbee switch** (or any source that doesn’t
|
||||
|
||||
The function only sends a message when a mapped light’s state **changed**, so multiple entities can be updated in one NVL cycle. When you use the Zigbee switch (or the PLC changes state any other way), the matching HA entities are updated.
|
||||
|
||||
### How to configure the Action node
|
||||
|
||||
You can use **one** Action node for both turn_on and turn_off (the sync sends the right `action` and `target` in each message). Connect **both** outputs of the sync function to the **same** Action node.
|
||||
|
||||
1. Add an **Action** node (Palette → Home Assistant → Action).
|
||||
2. **Server:** Select your Home Assistant server config.
|
||||
3. **Block Input Overrides:** Set to **off** (unchecked). This allows `msg.payload` from the sync to override the node config, so `payload.action` and `payload.target.entity_id` are used.
|
||||
4. **Action:** The node may require a non-empty value. Enter a placeholder such as **`input_boolean.turn_on`**. With **Block Input Overrides** off, the sync’s `msg.payload.action` overrides this on each message. Leave **Entity**, **Data** etc. empty.
|
||||
5. **Queue:** Optional — e.g. `none` or `last` if you want to send the last message when HA reconnects.
|
||||
6. Connect **Output 1** and **Output 2** of the NVL→HA sync function to this Action node.
|
||||
|
||||
Alternatively use **two** Action nodes: connect Output 1 to one (optional: set action to `input_boolean.turn_on`), Output 2 to the other (optional: `input_boolean.turn_off`). Still set **Block Input Overrides** to **off** so `payload.target.entity_id` is used.
|
||||
|
||||
---
|
||||
|
||||
## Connection to Flow 1 (CODESYS)
|
||||
|
||||
Reference in New Issue
Block a user