Implement global room configuration for Node-RED integration
- Introduced a single global room configuration file to streamline the management of rooms and lights, reducing redundancy in configuration. - Updated various integration scripts to utilize the global configuration, enhancing maintainability and clarity. - Improved documentation to reflect the new configuration approach, including instructions for loading and modifying the room configuration. This update simplifies the integration process and improves the overall user experience for managing lights and rooms in Node-RED.
This commit is contained in:
@@ -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)
|
## 1. How it works (recap)
|
||||||
|
|
||||||
- **HA / Zigbee** → Node-RED updates **`flow.nvlInState.rooms.<roomKey>`** (e.g. `ha_l1_on`, `zigbee_sw2`).
|
- **HA / Zigbee** → Node-RED updates **`flow.nvlInState.rooms.<roomKey>`** (e.g. `ha_l1_on`, `zigbee_sw2`).
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
// Writes to state.rooms.cmd_livingroom (NVL variable cmd_livingroom).
|
// Writes to state.rooms.<roomKey>. Room from entity_id via global roomConfig.entityToRoom. See /root/.node-red/room-config.js.
|
||||||
const ROOM_NAME = 'cmd_livingroom';
|
|
||||||
const entityId = (msg.topic || '').toString();
|
const entityId = (msg.topic || '').toString();
|
||||||
const parts = entityId.split('_');
|
const config = global.get('roomConfig');
|
||||||
const last = parts[parts.length - 1] || '';
|
const entityToRoom = (config && config.entityToRoom) ? config.entityToRoom : { living_room_new: 'cmd_livingroom' };
|
||||||
const num = parseInt(last, 10);
|
// Parse entity_id e.g. input_boolean.living_room_1 → base "living_room_1", then room + light
|
||||||
const lightNum = (!isNaN(num) && num >= 1) ? Math.min(6, num) : 1;
|
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
|
// Normalize state: HA / trigger-state can send payload as string, object with .state, or data.new_state.state
|
||||||
let rawState = msg.payload;
|
let rawState = msg.payload;
|
||||||
@@ -24,7 +27,7 @@ if (isOn) r['ha_l' + lightNum + '_on'] = true;
|
|||||||
else r['ha_l' + lightNum + '_off'] = true;
|
else r['ha_l' + lightNum + '_off'] = true;
|
||||||
flow.set('nvlInState', state);
|
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).
|
// 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 };
|
msg.payload = { buildAndSend: true };
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
// Run this AFTER nvl-receive. Syncs all mapped lights from PLC state to HA entities.
|
// 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).
|
// 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 config = global.get('roomConfig');
|
||||||
const LIGHT_ENTITY_MAP = [
|
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: 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 || {};
|
const payload = msg.payload || {};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Build the payload for nvl-send from flow.nvlInState.
|
// 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
|
// Wire: HA to NVL / Zigbee to NVL → this function → nvl-send → udp out
|
||||||
|
|
||||||
const struct_room_cmds_default = {
|
const struct_room_cmds_default = {
|
||||||
@@ -11,8 +11,8 @@ const struct_room_cmds_default = {
|
|||||||
ha_all_on: false, ha_all_off: false
|
ha_all_on: false, ha_all_off: false
|
||||||
};
|
};
|
||||||
|
|
||||||
// NVL variable names per room (struct_room_cmds). Add more when you add rooms.
|
const config = global.get('roomConfig');
|
||||||
const roomNames = ['cmd_livingroom'];
|
const roomNames = (config && config.roomNames && config.roomNames.length) ? config.roomNames : ['cmd_livingroom'];
|
||||||
|
|
||||||
const state = flow.get('nvlInState') || { rooms: {}, boiler: {} };
|
const state = flow.get('nvlInState') || { rooms: {}, boiler: {} };
|
||||||
const rooms = state.rooms || {};
|
const rooms = state.rooms || {};
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
// Writes to state.rooms.cmd_livingroom (same as HA to NVL). Zigbee action → zigbee_sw1..6.
|
// Writes to state.rooms.<roomKey>. Room from Zigbee device via global roomConfig.deviceToRoom. See /root/.node-red/room-config.js.
|
||||||
const ROOM_NAME = 'cmd_livingroom';
|
|
||||||
const actionToSwitch = { single: 1, double: 2, hold: 3, release: 4, triple: 5, quad: 6 };
|
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 payload = msg.payload || {};
|
||||||
const actionRaw = (payload.action || payload.click || '').toLowerCase();
|
const actionRaw = (payload.action || payload.click || '').toLowerCase();
|
||||||
// Support "1_single", "2_double" etc. (button_number + action) or plain "single", "double"
|
// 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;
|
state.rooms[ROOM_NAME]['zigbee_sw' + swIndex] = true;
|
||||||
flow.set('nvlInState', state);
|
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.payload = { buildAndSend: true };
|
||||||
msg.zigbeeClear = { room: ROOM_NAME, key: 'zigbee_sw' + swIndex };
|
msg.zigbeeClear = { room: ROOM_NAME, key: 'zigbee_sw' + swIndex };
|
||||||
|
|||||||
13
node-red/room-config-loader.js
Normal file
13
node-red/room-config-loader.js
Normal file
@@ -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;
|
||||||
69
node-red/room-config.js
Normal file
69
node-red/room-config.js
Normal file
@@ -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;
|
||||||
Reference in New Issue
Block a user