diff --git a/docs/integration/nodered-flows.json b/docs/integration/nodered-flows.json index 129c3ed..112bb19 100644 --- a/docs/integration/nodered-flows.json +++ b/docs/integration/nodered-flows.json @@ -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": [] } ] \ No newline at end of file diff --git a/docs/integration/nodered-livingroom-analysis.md b/docs/integration/nodered-livingroom-analysis.md new file mode 100644 index 0000000..17b057b --- /dev/null +++ b/docs/integration/nodered-livingroom-analysis.md @@ -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`. diff --git a/docs/integration/nodered-livingroom-flow.md b/docs/integration/nodered-livingroom-flow.md index 1d831aa..9bff290 100644 --- a/docs/integration/nodered-livingroom-flow.md +++ b/docs/integration/nodered-livingroom-flow.md @@ -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)