diff --git a/docs/integration/all-lights-and-rooms.md b/docs/integration/all-lights-and-rooms.md index ae40154..338106e 100644 --- a/docs/integration/all-lights-and-rooms.md +++ b/docs/integration/all-lights-and-rooms.md @@ -4,6 +4,20 @@ This doc explains how to scale the working “test light” setup to **all your --- +## 0. Single global room config (recommended) + +To avoid adding a new room or light in several places, use **one config file** that lives on the Node-RED server and is loaded into **global context** at startup. All function nodes read from `global.get('roomConfig')`. + +**Config file:** [`node-red/room-config.js`](../../node-red/room-config.js) — exports `ROOM_CONFIG` with: **roomNames** (NVL SEND), **lightEntityMap** (NVL to HA Sync), **entityToRoom** (HA to NVL), **deviceToRoom** (Zigbee to NVL). + +**Server location:** `/root/.node-red/room-config.js` (uploaded via `scp`). + +**Loader:** [`node-red/room-config-loader.js`](../../node-red/room-config-loader.js) — paste this code into a **Function node** named "Load room config". Connect an **Inject node** (once after 0.5 s) to it and deploy. It `require()`s the config file, clears the cache (so edits are picked up on redeploy), and sets `global.set('roomConfig', ...)`. + +**Adding a room or light:** Edit `room-config.js` locally, `scp` it to the server, and redeploy (or restart Node-RED). No function nodes need editing. + +--- + ## 1. How it works (recap) - **HA / Zigbee** → Node-RED updates **`flow.nvlInState.rooms.`** (e.g. `ha_l1_on`, `zigbee_sw2`). diff --git a/docs/integration/ha-to-nvl-function.js b/docs/integration/ha-to-nvl-function.js index 9f22f2b..ceaf417 100644 --- a/docs/integration/ha-to-nvl-function.js +++ b/docs/integration/ha-to-nvl-function.js @@ -1,10 +1,13 @@ -// Writes to state.rooms.cmd_livingroom (NVL variable cmd_livingroom). -const ROOM_NAME = 'cmd_livingroom'; +// Writes to state.rooms.. Room from entity_id via global roomConfig.entityToRoom. See /root/.node-red/room-config.js. const entityId = (msg.topic || '').toString(); -const parts = entityId.split('_'); -const last = parts[parts.length - 1] || ''; -const num = parseInt(last, 10); -const lightNum = (!isNaN(num) && num >= 1) ? Math.min(6, num) : 1; +const config = global.get('roomConfig'); +const entityToRoom = (config && config.entityToRoom) ? config.entityToRoom : { living_room_new: 'cmd_livingroom' }; +// Parse entity_id e.g. input_boolean.living_room_1 → base "living_room_1", then room + light +const base = (entityId.split('.')[1] || '').replace(/\s/g, ''); +const numMatch = base.match(/_(\d+)$/); +const lightNum = (numMatch && parseInt(numMatch[1], 10) >= 1) ? Math.min(6, parseInt(numMatch[1], 10)) : 1; +const baseWithoutNum = base.replace(/_?\d+$/, '') || base; +const ROOM_NAME = entityToRoom[baseWithoutNum] || entityToRoom[base] || 'cmd_livingroom'; // Normalize state: HA / trigger-state can send payload as string, object with .state, or data.new_state.state let rawState = msg.payload; @@ -24,7 +27,7 @@ if (isOn) r['ha_l' + lightNum + '_on'] = true; else r['ha_l' + lightNum + '_off'] = true; flow.set('nvlInState', state); -node.warn('[HA to NVL] set cmd_livingroom ha_l' + lightNum + '_' + (isOn ? 'on' : 'off') + '=true, buildAndSend'); +node.warn('[HA to NVL] set ' + ROOM_NAME + ' ha_l' + lightNum + '_' + (isOn ? 'on' : 'off') + '=true, buildAndSend'); // 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). msg.payload = { buildAndSend: true }; diff --git a/docs/integration/nvl-to-ha-sync-livingroom.js b/docs/integration/nvl-to-ha-sync-livingroom.js index 406b1dd..03c0d52 100644 --- a/docs/integration/nvl-to-ha-sync-livingroom.js +++ b/docs/integration/nvl-to-ha-sync-livingroom.js @@ -1,13 +1,10 @@ // Run this AFTER nvl-receive. Syncs all mapped lights from PLC state to HA entities. +// Map comes from global roomConfig (single source of truth). See /root/.node-red/room-config.js. // Connect output 1 → Action node (turn_on), output 2 → Action node (turn_off). -// Format matches node-red-contrib-home-assistant-websocket "Action" node: payload.action + payload.target.entity_id (array). -// Only outputs when state changed (per entity). Add more entries to LIGHT_ENTITY_MAP as you add rooms/lights. -// Map: NVL room key (in payload) + light index 1..6 → HA entity_id -const LIGHT_ENTITY_MAP = [ +const config = global.get('roomConfig'); +const LIGHT_ENTITY_MAP = (config && config.lightEntityMap && config.lightEntityMap.length) ? config.lightEntityMap : [ { room: 'light_livingRoom', light: 1, entityId: 'input_boolean.living_room_new' }, - // { room: 'light_livingRoom', light: 2, entityId: 'input_boolean.living_room_2' }, - // { room: 'l_kitchen', light: 1, entityId: 'light.kitchen_ceiling' }, ]; const payload = msg.payload || {}; diff --git a/docs/integration/state-to-nvl-send-payload.js b/docs/integration/state-to-nvl-send-payload.js index a9c54a3..53c7037 100644 --- a/docs/integration/state-to-nvl-send-payload.js +++ b/docs/integration/state-to-nvl-send-payload.js @@ -1,5 +1,5 @@ // Build the payload for nvl-send from flow.nvlInState. -// Redesign: all rooms use struct_room_cmds. Add more names to roomNames when you add rooms. +// Room list comes from global roomConfig (single source of truth). See /root/.node-red/room-config.js. // Wire: HA to NVL / Zigbee to NVL → this function → nvl-send → udp out const struct_room_cmds_default = { @@ -11,8 +11,8 @@ const struct_room_cmds_default = { ha_all_on: false, ha_all_off: false }; -// NVL variable names per room (struct_room_cmds). Add more when you add rooms. -const roomNames = ['cmd_livingroom']; +const config = global.get('roomConfig'); +const roomNames = (config && config.roomNames && config.roomNames.length) ? config.roomNames : ['cmd_livingroom']; const state = flow.get('nvlInState') || { rooms: {}, boiler: {} }; const rooms = state.rooms || {}; diff --git a/docs/integration/zigbee-to-nvl-function.js b/docs/integration/zigbee-to-nvl-function.js index 1818cfa..bb8df3a 100644 --- a/docs/integration/zigbee-to-nvl-function.js +++ b/docs/integration/zigbee-to-nvl-function.js @@ -1,6 +1,9 @@ -// Writes to state.rooms.cmd_livingroom (same as HA to NVL). Zigbee action → zigbee_sw1..6. -const ROOM_NAME = 'cmd_livingroom'; +// Writes to state.rooms.. Room from Zigbee device via global roomConfig.deviceToRoom. See /root/.node-red/room-config.js. const actionToSwitch = { single: 1, double: 2, hold: 3, release: 4, triple: 5, quad: 6 }; +const config = global.get('roomConfig'); +const deviceToRoom = (config && config.deviceToRoom) ? config.deviceToRoom : { 'Office Switch': 'cmd_livingroom' }; +const friendlyName = (msg.payload && msg.payload.friendly_name) ? msg.payload.friendly_name : ''; +const ROOM_NAME = deviceToRoom[friendlyName] || 'cmd_livingroom'; const payload = msg.payload || {}; const actionRaw = (payload.action || payload.click || '').toLowerCase(); // Support "1_single", "2_double" etc. (button_number + action) or plain "single", "double" @@ -25,7 +28,7 @@ if (!state.rooms[ROOM_NAME]) state.rooms[ROOM_NAME] = {}; state.rooms[ROOM_NAME]['zigbee_sw' + swIndex] = true; flow.set('nvlInState', state); -node.warn('[Zigbee to NVL] cmd_livingroom zigbee_sw' + swIndex + '=true (action=' + actionRaw + '), buildAndSend + zigbeeClear'); +node.warn('[Zigbee to NVL] ' + ROOM_NAME + ' zigbee_sw' + swIndex + '=true (action=' + actionRaw + '), buildAndSend + zigbeeClear'); msg.payload = { buildAndSend: true }; msg.zigbeeClear = { room: ROOM_NAME, key: 'zigbee_sw' + swIndex }; diff --git a/node-red/room-config-loader.js b/node-red/room-config-loader.js new file mode 100644 index 0000000..17eda52 --- /dev/null +++ b/node-red/room-config-loader.js @@ -0,0 +1,13 @@ +// Paste this into a Function node named "Load room config". +// Connect an Inject node (once after 0.5 s) to it so it runs at startup. +// +// Since functionExternalModules is true in settings.js, you can require files. +// room-config.js must be on the Node-RED server at /root/.node-red/room-config.js + +// Delete cached version so changes to the file are picked up on redeploy +delete require.cache[require.resolve('/root/.node-red/room-config.js')]; +const ROOM_CONFIG = require('/root/.node-red/room-config.js'); + +global.set('roomConfig', ROOM_CONFIG); +node.warn('[Load room config] loaded: ' + ROOM_CONFIG.roomNames.length + ' rooms, ' + ROOM_CONFIG.lightEntityMap.length + ' light mappings'); +return null; diff --git a/node-red/room-config.js b/node-red/room-config.js new file mode 100644 index 0000000..5d5098d --- /dev/null +++ b/node-red/room-config.js @@ -0,0 +1,69 @@ +/** + * Single source of truth for rooms and lights. + * Lives on the Node-RED server at /root/.node-red/room-config.js + * All function nodes use: global.get('roomConfig') + * Loaded at startup by room-config-loader.js (Function node + Inject once). + * + * When you add a room or light, edit ONLY this file, upload to the server, and restart Node-RED (or redeploy). + */ + +const ROOM_CONFIG = { + // Order must match CODESYS NVL_In. Used by NVL SEND (state-to-nvl-send-payload). + roomNames: [ + 'cmd_livingroom', // test slot — replace with 'livingRoom' when moving to production + 'masterBedroom', + 'masterBathroom', + 'bedroom_1', + 'bedroom_2', + 'bathroom', + 'guestWc', + 'kitchen', + 'pantry', + 'livingRoom', + 'dinningRoom', + 'entrance', + 'hallway', + 'outVeranda', + 'outFront', + 'outBack', + 'outSide', + ], + + // PLC → HA sync. room = key in nvl-receive payload, light = 1..6, entityId = HA entity. + lightEntityMap: [ + { room: 'light_livingRoom', light: 1, entityId: 'input_boolean.living_room_new' }, + // { room: 'light_livingRoom', light: 2, entityId: 'input_boolean.living_room_2' }, + // { room: 'l_kitchen', light: 1, entityId: 'input_boolean.kitchen_1' }, + ], + + // HA entity_id substring (after domain.) without trailing _N → NVL room key. For HA to NVL. + entityToRoom: { + living_room: 'livingRoom', + living_room_new: 'cmd_livingroom', + kitchen: 'kitchen', + master_bedroom: 'masterBedroom', + bathroom: 'bathroom', + bedroom_1: 'bedroom_1', + bedroom_2: 'bedroom_2', + dinning_room: 'dinningRoom', + entrance: 'entrance', + hallway: 'hallway', + pantry: 'pantry', + guest_wc: 'guestWc', + out_veranda: 'outVeranda', + out_front: 'outFront', + out_back: 'outBack', + out_side: 'outSide', + }, + + // Zigbee device friendly_name → NVL room key. For Zigbee to NVL. + deviceToRoom: { + 'Living Room Door Switch': 'livingRoom', + 'Office Switch': 'cmd_livingroom', + 'Kitchen Switch': 'kitchen', + 'Master Bedroom Switch': 'masterBedroom', + // Add each Zigbee switch friendly_name and the room key it controls. + }, +}; + +module.exports = ROOM_CONFIG;