Enhance Zigbee to NVL integration with button mapping and configuration updates
- Updated documentation to clarify the mapping of Zigbee buttons to specific (room, light) pairs using `switchBindings`. - Improved the Zigbee to NVL function to support both single-device and multi-device payloads, enhancing flexibility in handling actions. - Revised the room configuration to include detailed switch bindings and fallback mechanisms for device identification, streamlining the integration process. This update improves the usability and functionality of the Zigbee integration within Node-RED, facilitating better control of lighting systems.
This commit is contained in:
@@ -116,25 +116,28 @@ const lightNum = parseInt((name.match(/_(\d+)$/) || [,'1'])[1], 10) || 1;
|
||||
|
||||
Then set `state.rooms[roomKey]['ha_l' + lightNum + '_on']` or `_off` as you do now. So one HA to NVL function can serve all entities if you pass the right `msg.topic` and use a consistent naming (e.g. `room_light_N`).
|
||||
|
||||
### 3.3 Zigbee → PLC: which room?
|
||||
### 3.3 Zigbee → PLC: which light does each button control?
|
||||
|
||||
- Each **zigbee2mqtt-in** is one physical switch.
|
||||
- **Zigbee to NVL** must know which **room** that switch belongs to.
|
||||
- Either: **one Zigbee to NVL per room** (each has `ROOM_NAME = 'kitchen'` etc.) and wire each switch to the right one, or **one Zigbee to NVL** that gets **device/entity** from `msg` and uses a **device → room** map (e.g. `friendly_name` or `device_id` → room key).
|
||||
Each **button** (1..6) is mapped to a specific **(room, light)** so e.g. Office switch button 1 → cmd_livingroom light 1, button 2 → cmd_out light 2. Config is in **room-config.js** → **`switchBindings`**:
|
||||
|
||||
Example map in Zigbee to NVL:
|
||||
- Key: Zigbee device **friendly_name**.
|
||||
- Value: object mapping **button index** (1..6) to **`{ room, light }`** or to an array **`[ { room, light }, ... ]`** (one button can control several lights).
|
||||
- The PLC receives `zigbee_sw<light>` set in the given **room** for each target.
|
||||
|
||||
**Example in `room-config.js`:**
|
||||
|
||||
```javascript
|
||||
const DEVICE_TO_ROOM = {
|
||||
'Living Room Door Switch': 'livingRoom',
|
||||
'Office Switch': 'livingRoom', // or 'office' if you add that room
|
||||
'Kitchen Switch': 'kitchen',
|
||||
// ...
|
||||
};
|
||||
const roomKey = DEVICE_TO_ROOM[msg.payload?.friendly_name || ''] || 'livingRoom';
|
||||
switchBindings: {
|
||||
'Office Switch': {
|
||||
1: { room: 'cmd_livingroom', light: 1 }, // button 1 → light 1 in cmd_livingroom
|
||||
2: { room: 'cmd_out', light: 2 }, // button 2 → light 2 in cmd_out
|
||||
// One button, multiple lights:
|
||||
// 3: [ { room: 'kitchen', light: 1 }, { room: 'hallway', light: 1 } ],
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
Then use `roomKey` instead of a fixed `ROOM_NAME` when writing to `state.rooms[roomKey]`.
|
||||
Add every **room** you use (e.g. `cmd_out`) to **`roomNames`** so NVL SEND includes it. If a device has no `switchBindings` entry, Zigbee to NVL falls back to **`deviceToRoom`** and uses the button index as the light number in that single room. Use **[clear-zigbee-edge.js](clear-zigbee-edge.js)** in the Clear zigbee edge node so it supports an array of `{ room, key }` (multi-target).
|
||||
|
||||
### 3.4 PLC → HA: NVL to HA Sync (LIGHT_ENTITY_MAP)
|
||||
|
||||
@@ -263,7 +266,7 @@ Use a **consistent pattern** so you can derive room + light in Node-RED and in Y
|
||||
| 2 | **HA** | Create all entities (YAML package or UI). Use a clear naming scheme (e.g. `room_light_N`). |
|
||||
| 3 | **Node-RED – NVL SEND** | Set **`roomNames`** in `state-to-nvl-send-payload.js` to the full list of room keys (same order as PLC). |
|
||||
| 4 | **Node-RED – HA to NVL** | Either duplicate the flow per room (Option A) or use one flow and derive **room + light** from `msg.topic` (Option B). Ensure every HA entity is mapped to a room key and light index. |
|
||||
| 5 | **Node-RED – Zigbee to NVL** | Map each Zigbee device (by name or id) to a **room key** so the correct `state.rooms[roomKey]` is updated. |
|
||||
| 5 | **Node-RED – Zigbee to NVL** | In **room-config.js** set **switchBindings** (device → button → `{ room, light }` or array) so each button controls the right room+light; add those rooms to **roomNames**. Use **clear-zigbee-edge.js** for the 80 ms clear. |
|
||||
| 6 | **Node-RED – NVL to HA Sync** | Fill **LIGHT_ENTITY_MAP** with one entry per light: `room` (payload key from nvl-receive), `light` (1..6), `entityId` (HA entity). |
|
||||
| 7 | **Action nodes** | Keep one Action node (or two for on/off) with **Block Input Overrides** off so the sync’s `payload.action` and `payload.target.entity_id` are used for all entities. |
|
||||
|
||||
|
||||
15
docs/integration/clear-zigbee-edge.js
Normal file
15
docs/integration/clear-zigbee-edge.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// Paste into the "Clear zigbee edge" Function node (after 80 ms delay).
|
||||
// Clears zigbee_swN in nvlInState so the PLC sees one pulse. Supports one room or many (switchBindings).
|
||||
// msg.zigbeeClear can be: { room, key } or [ { room, key }, ... ]
|
||||
|
||||
if (msg.zigbeeClear) {
|
||||
const state = flow.get('nvlInState') || { rooms: {}, boiler: {} };
|
||||
const list = Array.isArray(msg.zigbeeClear) ? msg.zigbeeClear : [msg.zigbeeClear];
|
||||
for (const item of list) {
|
||||
const r = state.rooms[item.room];
|
||||
if (r && item.key) r[item.key] = false;
|
||||
}
|
||||
flow.set('nvlInState', state);
|
||||
}
|
||||
msg.payload = { buildAndSend: true };
|
||||
return msg;
|
||||
81
docs/integration/nodered-flow-check-report.md
Normal file
81
docs/integration/nodered-flow-check-report.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Node-RED flow check report (live vs repo)
|
||||
|
||||
Checked live flows on **10.20.30.12** (Feb 2026). Summary below.
|
||||
|
||||
---
|
||||
|
||||
## Updated (match repo) – Living Room tab
|
||||
|
||||
| Node name | Status | Note |
|
||||
|----------------------|----------|------|
|
||||
| HA to NVL | Updated | Uses `roomConfig`, `entityToRoom` |
|
||||
| Zigbee to NVL | Updated | Uses `switchBindings`, multi-device payload, `v.payload.action` |
|
||||
| Clear zigbee edge | Updated | Supports `Array.isArray(msg.zigbeeClear)` |
|
||||
| STATE TO NVL | Updated | Uses `global.get('roomConfig').roomNames` |
|
||||
| NVL to HA Sync | Updated | Uses `roomConfig.lightEntityMap` |
|
||||
|
||||
You have **duplicate** nodes in the same tab: some of these names exist twice (or three times), and **one copy is old**. The flow may be wired to the **old** instances.
|
||||
|
||||
---
|
||||
|
||||
## Not updated / needs action
|
||||
|
||||
### 1. Load room config – **empty**
|
||||
|
||||
- **Current:** The "Load room config" function node contains only `return msg;` (12 chars).
|
||||
- **Effect:** `global.get('roomConfig')` is never set from `room-config.js`, so other nodes may use fallbacks or wrong data.
|
||||
- **Fix:** Paste the full loader code into the "Load room config" function node. Code to paste is in **[node-red/room-config-loader.js](../../node-red/room-config-loader.js)** (see below).
|
||||
|
||||
### 2. Duplicate nodes in Living Room
|
||||
|
||||
Same tab contains both updated and old versions:
|
||||
|
||||
- **HA to NVL:** 1 updated, 1 old
|
||||
- **Zigbee to NVL:** 2 updated, 1 old
|
||||
- **Clear zigbee edge:** 1 updated, 1 old
|
||||
- **NVL to HA Sync:** 1 updated, 1 old
|
||||
- **NVL SEND:** 1 node, **old** (hardcoded `roomNames = ['cmd_livingroom']`)
|
||||
|
||||
If your flow is connected to the **old** nodes, behaviour will not match the repo. Ensure:
|
||||
|
||||
- Zigbee/HA → **updated** "Zigbee to NVL" / "HA to NVL" → **updated** "STATE TO NVL" (or replace "NVL SEND" with STATE TO NVL and wire that) → nvl-send.
|
||||
- Delay → **updated** "Clear zigbee edge" (the one that has `Array.isArray(msg.zigbeeClear)`).
|
||||
|
||||
### 3. Flow 7
|
||||
|
||||
All relevant function nodes there are **old** (fixed `livingRoom`, no roomConfig). Update them if you use that tab, or leave as-is if unused.
|
||||
|
||||
---
|
||||
|
||||
## Code to paste: Load room config
|
||||
|
||||
Paste this into the **Load room config** function node (and connect an Inject “once after 0.5 s” to it):
|
||||
|
||||
```javascript
|
||||
// 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;
|
||||
```
|
||||
|
||||
Full file: [node-red/room-config-loader.js](../../node-red/room-config-loader.js).
|
||||
|
||||
---
|
||||
|
||||
## room-config.js on server
|
||||
|
||||
`/root/.node-red/room-config.js` is present and contains `roomNames`, `switchBindings`, etc. Upload from repo when you change config:
|
||||
`scp node-red/room-config.js root@10.20.30.12:/root/.node-red/room-config.js`
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
1. [ ] Paste **Load room config** code (above) into the "Load room config" node and deploy.
|
||||
2. [ ] In Living Room, confirm wires go to the **updated** HA to NVL / Zigbee to NVL / Clear zigbee edge (not the old duplicates).
|
||||
3. [ ] Use **STATE TO NVL** (roomConfig) in the NVL path, or replace **NVL SEND** content with [state-to-nvl-send-payload.js](state-to-nvl-send-payload.js).
|
||||
4. [ ] Optionally remove or disable the **old** duplicate nodes in Living Room to avoid confusion.
|
||||
5. [ ] After changing `room-config.js`, upload it and redeploy (or restart Node-RED).
|
||||
@@ -103,9 +103,10 @@ If `isOn` is false when you expect ON, check that the trigger-state output is ex
|
||||
|
||||
## Zigbee to NVL (function code)
|
||||
|
||||
- **Room:** `livingRoom`.
|
||||
- **Action map:** `single→1, double→2, hold→3, release→4, triple→5, quad→6` → sets `zigbee_swN`.
|
||||
- **Edge behaviour:** Passes `msg.zigbeeClear = { room: 'livingRoom', key: 'zigbee_swN' }` so **Clear zigbee edge** (after 80 ms) clears that flag and calls **Build NVL_In** again; PLC sees one pulse.
|
||||
- Uses **room-config.js** `switchBindings` (and optional `deviceIdToName` / `switchBindingsByDeviceId`) so each button maps to (room, light). See [zigbee-to-nvl-function.js](zigbee-to-nvl-function.js).
|
||||
- **Payload formats supported:** (1) single device `{ action: "1_single", friendly_name: "Office Switch" }`; (2) multi-device object keyed by IEEE `{ "0xa4c...": { action, ... } }`; (3) plain action string `"1_single"` with device name in `msg.topic` (e.g. `zigbee2mqtt/Office Switch/action`).
|
||||
- **If your multi-device node never sends `action`:** the node may only output device list/state. Use an **MQTT In** node subscribed to `zigbee2mqtt/+/action` and feed that into Zigbee to NVL; then `msg.payload` will be the action string (e.g. `1_single`) and `msg.topic` will be e.g. `zigbee2mqtt/Office Switch/action` (the function parses the friendly name from the topic).
|
||||
- **Edge behaviour:** Passes `msg.zigbeeClear` (single object or array) so **Clear zigbee edge** (after 80 ms) clears the flags; use [clear-zigbee-edge.js](clear-zigbee-edge.js).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,35 +1,108 @@
|
||||
// Writes to state.rooms.<roomKey>. Room from Zigbee device via global roomConfig.deviceToRoom. See /root/.node-red/room-config.js.
|
||||
// Each button maps to (room, light) via global roomConfig.switchBindings. See /root/.node-red/room-config.js.
|
||||
// Supports single-device payload (action, friendly_name at top level) and multi-device payload (keyed by IEEE address).
|
||||
const actionToSwitch = { single: 1, double: 2, hold: 3, release: 4, triple: 5, quad: 6 };
|
||||
const config = global.get('roomConfig');
|
||||
const switchBindings = (config && config.switchBindings) ? config.switchBindings : {};
|
||||
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"
|
||||
const parts = actionRaw.split('_');
|
||||
let swIndex = null;
|
||||
if (parts.length >= 2) {
|
||||
const buttonNum = parseInt(parts[0], 10);
|
||||
if (buttonNum >= 1 && buttonNum <= 6) swIndex = buttonNum;
|
||||
}
|
||||
if (swIndex == null) swIndex = actionToSwitch[actionRaw];
|
||||
if (swIndex == null && parts.length >= 2) swIndex = actionToSwitch[parts.slice(1).join('_')];
|
||||
if (swIndex == null) swIndex = actionToSwitch[parts[parts.length - 1]];
|
||||
// Optional: map IEEE address to friendly name when multi-device node doesn't provide it. e.g. deviceIdToName: { '0xa4c138a5b9771b05': 'Office Switch' }
|
||||
const deviceIdToName = (config && config.deviceIdToName) ? config.deviceIdToName : {};
|
||||
|
||||
if (swIndex == null) {
|
||||
node.warn('[Zigbee to NVL] unknown action: ' + JSON.stringify(actionRaw) + ' payload=' + JSON.stringify(payload));
|
||||
const payload = msg.payload != null ? msg.payload : {};
|
||||
// Payload can be: single-device { action, friendly_name }, multi-device { "0x...": { ... } }, or plain action string "1_single"
|
||||
let friendlyName = (typeof payload === 'object' && payload.friendly_name != null) ? payload.friendly_name.toString() : '';
|
||||
let actionRaw = (typeof payload === 'string' ? payload : (payload.action || payload.click || msg.action || (payload && payload.action)) || '').toString().toLowerCase();
|
||||
|
||||
// Multi-device payload: prefer the device that has an action AND a configured binding (so we don't pick another device's stale action)
|
||||
if (!actionRaw && typeof payload === 'object' && !Array.isArray(payload)) {
|
||||
const keys = Object.keys(payload);
|
||||
let fallback = null;
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
const v = payload[key];
|
||||
if (key && key.toLowerCase().indexOf('0x') === 0 && v && typeof v === 'object') {
|
||||
const inner = v.payload || v.message || v.state || v;
|
||||
const a = (inner.action || inner.click || v.action || v.click || '').toString().toLowerCase();
|
||||
if (!a) continue;
|
||||
const name = (v.friendly_name || v.name || (v.item && (v.item.friendly_name || v.item.name)) || deviceIdToName[key] || key).toString();
|
||||
if (switchBindings[name] || deviceToRoom[name]) {
|
||||
actionRaw = a;
|
||||
friendlyName = name;
|
||||
break;
|
||||
}
|
||||
if (!fallback) fallback = { actionRaw: a, friendlyName: name };
|
||||
}
|
||||
}
|
||||
if (!actionRaw && fallback) {
|
||||
actionRaw = fallback.actionRaw;
|
||||
friendlyName = fallback.friendlyName;
|
||||
}
|
||||
}
|
||||
// If still no action, try msg.data or msg.topic (some nodes put action in topic, e.g. "1_single")
|
||||
if (!actionRaw && msg.data && msg.data.action) actionRaw = msg.data.action.toString().toLowerCase();
|
||||
if (!actionRaw && typeof msg.topic === 'string' && /^\d_[a-z_]+$/.test(msg.topic.trim())) actionRaw = msg.topic.trim().toLowerCase();
|
||||
// If payload was the action string, get friendly_name from topic e.g. zigbee2mqtt/Office Switch/action
|
||||
if (actionRaw && !friendlyName && typeof msg.topic === 'string') {
|
||||
const parts = msg.topic.split('/');
|
||||
if (parts.length >= 2) friendlyName = parts[1].trim(); // zigbee2mqtt/Office Switch/action → Office Switch
|
||||
}
|
||||
|
||||
if (!actionRaw) {
|
||||
node.warn('[Zigbee to NVL] no action in payload. Keys: ' + (typeof payload === 'object' ? Object.keys(payload).join(', ') : 'n/a'));
|
||||
return null;
|
||||
}
|
||||
|
||||
const parts = actionRaw.split('_');
|
||||
let buttonIndex = null;
|
||||
if (parts.length >= 2) {
|
||||
const n = parseInt(parts[0], 10);
|
||||
if (n >= 1 && n <= 6) buttonIndex = n;
|
||||
}
|
||||
if (buttonIndex == null) buttonIndex = actionToSwitch[actionRaw];
|
||||
if (buttonIndex == null && parts.length >= 2) buttonIndex = actionToSwitch[parts.slice(1).join('_')];
|
||||
if (buttonIndex == null) buttonIndex = actionToSwitch[parts[parts.length - 1]];
|
||||
|
||||
if (buttonIndex == null) {
|
||||
node.warn('[Zigbee to NVL] unknown action: ' + JSON.stringify(actionRaw));
|
||||
return null;
|
||||
}
|
||||
|
||||
node.warn('[Zigbee to NVL] action=' + actionRaw + ' buttonIndex=' + buttonIndex + ' device=' + friendlyName);
|
||||
|
||||
// Prefer switchBindings by friendly name; fallback by device ID (IEEE) if we have switchBindingsByDeviceId
|
||||
const bindingByDeviceId = (config && config.switchBindingsByDeviceId) ? config.switchBindingsByDeviceId : null;
|
||||
let targets = [];
|
||||
let binding = switchBindings[friendlyName] && switchBindings[friendlyName][buttonIndex];
|
||||
if ((binding === undefined || binding === null) && bindingByDeviceId) {
|
||||
const deviceId = typeof payload === 'object' && Object.keys(payload).some(function (k) { return k.indexOf('0x') === 0; })
|
||||
? Object.keys(payload).find(function (k) { return k.indexOf('0x') === 0; })
|
||||
: null;
|
||||
if (deviceId) binding = bindingByDeviceId[deviceId] && bindingByDeviceId[deviceId][buttonIndex];
|
||||
}
|
||||
if (binding !== undefined && binding !== null) {
|
||||
targets = Array.isArray(binding) ? binding : [binding];
|
||||
targets = targets.filter(function (t) { return t && t.room && t.light >= 1 && t.light <= 6; });
|
||||
}
|
||||
if (targets.length === 0) {
|
||||
const room = deviceToRoom[friendlyName] || 'cmd_livingroom';
|
||||
targets = [{ room: room, light: buttonIndex }];
|
||||
node.warn('[Zigbee to NVL] no switchBindings for ' + friendlyName + ' btn' + buttonIndex + ', using fallback: ' + room + ' light ' + buttonIndex);
|
||||
}
|
||||
|
||||
if (!flow.get('nvlInState')) flow.set('nvlInState', { rooms: {}, boiler: {} });
|
||||
const state = flow.get('nvlInState');
|
||||
if (!state.rooms[ROOM_NAME]) state.rooms[ROOM_NAME] = {};
|
||||
state.rooms[ROOM_NAME]['zigbee_sw' + swIndex] = true;
|
||||
const clearList = [];
|
||||
|
||||
for (const t of targets) {
|
||||
const key = 'zigbee_sw' + t.light;
|
||||
if (!state.rooms[t.room]) state.rooms[t.room] = {};
|
||||
state.rooms[t.room][key] = true;
|
||||
clearList.push({ room: t.room, key: key });
|
||||
}
|
||||
|
||||
flow.set('nvlInState', state);
|
||||
|
||||
node.warn('[Zigbee to NVL] ' + ROOM_NAME + ' zigbee_sw' + swIndex + '=true (action=' + actionRaw + '), buildAndSend + zigbeeClear');
|
||||
node.warn('[Zigbee to NVL] ' + (friendlyName || 'device') + ' btn' + buttonIndex + ' → ' + targets.map(function (t) { return t.room + ' L' + t.light; }).join(', '));
|
||||
|
||||
msg.payload = { buildAndSend: true };
|
||||
msg.zigbeeClear = { room: ROOM_NAME, key: 'zigbee_sw' + swIndex };
|
||||
msg.zigbeeClear = clearList;
|
||||
return msg;
|
||||
|
||||
51
node-red/reload-room-config-docs.md
Normal file
51
node-red/reload-room-config-docs.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Reload room config without restarting Node-RED
|
||||
|
||||
After you edit `room-config.js` on the server (and save/upload it), you can have Node-RED reload it into `global.roomConfig` so all flows use the new config **without restarting Node-RED**.
|
||||
|
||||
---
|
||||
|
||||
## Option A: Auto-reload when you save the file (recommended)
|
||||
|
||||
When `room-config.js` is saved on the server, it is reloaded automatically. No extra nodes to install—use the built-in **Watch** node.
|
||||
|
||||
### Add the “Config reload on save” flow
|
||||
|
||||
Add these nodes and wire in order:
|
||||
|
||||
| Order | Node type | Name | Config |
|
||||
|-------|-----------|------|--------|
|
||||
| 1 | **watch** (built-in) | Watch room-config | In the node config, enter the **file** to watch. One of: **`/root/.node-red/room-config.js`** (single file) or **`/root/.node-red`** (directory; then add the filter Function below so only `room-config.js` triggers reload). The Watch node puts the changed file path in `msg.payload` and `msg.filename`, and the short name in `msg.file`. |
|
||||
| 2 | **Exec** | Read room-config as JSON | **Command:** `node -e "console.log(JSON.stringify(require('/root/.node-red/room-config.js')))"` · **Append:** payload (so output becomes `msg.payload`). |
|
||||
| 3 | **Function** | Reload room config from payload | Paste the code from **[reload-room-config-function.js](reload-room-config-function.js)**. |
|
||||
|
||||
**Wiring:** Watch → Exec → Reload room config from payload (Function).
|
||||
|
||||
If you watch the **directory** `/root/.node-red` instead of the single file, insert a **Function** between Watch and Exec so only changes to `room-config.js` trigger a reload:
|
||||
|
||||
```javascript
|
||||
// Only pass through when the changed file is room-config.js
|
||||
if (msg.file !== 'room-config.js') return null;
|
||||
return msg;
|
||||
```
|
||||
|
||||
After deploy, saving (or uploading) `/root/.node-red/room-config.js` will trigger the Watch → Exec → reload. Config updates with no restart and no manual reload.
|
||||
|
||||
---
|
||||
|
||||
## Option B: Manual reload (no file watcher)
|
||||
|
||||
Use this if you prefer not to install the watch node.
|
||||
|
||||
1. **Inject** – e.g. “Reload config” (click to trigger).
|
||||
2. **Exec** – same command as in Option A, step 3.
|
||||
3. **Function** – same “Reload room config from payload” code as in Option A, step 4.
|
||||
|
||||
**Wiring:** Inject → Exec → Function.
|
||||
|
||||
Trigger the Inject after you upload a new `room-config.js`.
|
||||
|
||||
---
|
||||
|
||||
## Note
|
||||
|
||||
`room-config.js` must export a plain object (no functions, no non-JSON values). The repo version is JSON-serializable.
|
||||
22
node-red/reload-room-config-function.js
Normal file
22
node-red/reload-room-config-function.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// Paste into a Function node named "Reload room config from payload".
|
||||
// Input: msg.payload = JSON string (from Exec node that runs the script below).
|
||||
// Sets global roomConfig so all flows see the new config without restarting Node-RED.
|
||||
|
||||
const raw = msg.payload;
|
||||
if (typeof raw !== 'string') {
|
||||
node.warn('[Reload config] expected string payload, got ' + typeof raw);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const config = JSON.parse(raw.trim());
|
||||
if (!config.roomNames || !Array.isArray(config.roomNames)) {
|
||||
node.error('[Reload config] invalid config: missing roomNames');
|
||||
return null;
|
||||
}
|
||||
global.set('roomConfig', config);
|
||||
node.warn('[Reload config] OK: ' + config.roomNames.length + ' rooms, ' + (config.lightEntityMap ? config.lightEntityMap.length : 0) + ' light mappings');
|
||||
return msg;
|
||||
} catch (e) {
|
||||
node.error('[Reload config] parse error: ' + e.message);
|
||||
return null;
|
||||
}
|
||||
@@ -1,13 +1,24 @@
|
||||
// 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
|
||||
// Load room config into global (no require - Function nodes can't use it).
|
||||
// 1. Open node-red/room-config.js in the repo.
|
||||
// 2. Copy the whole object: from "const ROOM_CONFIG = {" up to "};" (do NOT copy "module.exports").
|
||||
// 3. Replace the ROOM_CONFIG below with your paste. Keep the two lines at the end.
|
||||
|
||||
// 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');
|
||||
const ROOM_CONFIG = {
|
||||
roomNames: ['cmd_livingroom', 'cmd_out'],
|
||||
lightEntityMap: [
|
||||
{ room: 'light_livingRoom', light: 1, entityId: 'input_boolean.living_room_new' },
|
||||
],
|
||||
entityToRoom: { living_room_new: 'cmd_livingroom' },
|
||||
deviceToRoom: { 'Office Switch': 'cmd_livingroom' },
|
||||
deviceIdToName: {},
|
||||
switchBindings: {
|
||||
'Office Switch': {
|
||||
1: { room: 'cmd_livingroom', light: 1 },
|
||||
2: { room: 'cmd_livingroom', light: 1 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
global.set('roomConfig', ROOM_CONFIG);
|
||||
node.warn('[Load room config] loaded: ' + ROOM_CONFIG.roomNames.length + ' rooms, ' + ROOM_CONFIG.lightEntityMap.length + ' light mappings');
|
||||
node.warn('[Load room config] loaded: ' + ROOM_CONFIG.roomNames.length + ' rooms');
|
||||
return null;
|
||||
|
||||
@@ -2,31 +2,31 @@
|
||||
* 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).
|
||||
* Loaded at startup from settings.js (functionGlobalContext). To reload without restart, use the
|
||||
* "Reload room config" flow: see node-red/reload-room-config-docs.md
|
||||
*/
|
||||
|
||||
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',
|
||||
'cmd_livingroom',
|
||||
'cmd_out', // add other NVL room keys as needed
|
||||
// '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.
|
||||
@@ -38,31 +38,49 @@ const ROOM_CONFIG = {
|
||||
|
||||
// HA entity_id substring (after domain.) without trailing _N → NVL room key. For HA to NVL.
|
||||
entityToRoom: {
|
||||
living_room: 'livingRoom',
|
||||
// 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',
|
||||
// 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.
|
||||
// Zigbee: friendly_name → single room (fallback when switchBindings missing).
|
||||
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.
|
||||
},
|
||||
|
||||
// When using a multi-device Zigbee node, payload is keyed by IEEE (e.g. 0xa4c138a5b9771b05). Map IEEE → friendly name so switchBindings by name still works.
|
||||
deviceIdToName: {
|
||||
// '0xa4c138a5b9771b05': 'Office Switch', // uncomment and add your device IEEE from the payload
|
||||
},
|
||||
|
||||
// Optional: bind by IEEE instead of friendly name (same shape as switchBindings). Use if the node never sends friendly_name.
|
||||
// switchBindingsByDeviceId: {
|
||||
// '0xa4c138a5b9771b05': { 1: { room: 'cmd_livingroom', light: 1 }, 2: { room: 'cmd_out', light: 2 } },
|
||||
// },
|
||||
|
||||
/**
|
||||
* Which (room, light) each physical button controls. Key = button number (1..6), value = { room, light }.
|
||||
* Button and light are independent: e.g. button 2 can control light 1, or light 2 in another room.
|
||||
* One button can target one { room, light } or an array for multiple lights.
|
||||
*/
|
||||
switchBindings: {
|
||||
'Office Switch': {
|
||||
1: { room: 'cmd_livingroom', light: 1 }, // button 1 → light 1
|
||||
2: { room: 'cmd_livingroom', light: 1 }, // button 2 → light 1 (same light; other switches could do button 2 → light 2)
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
34
node-red/settings-roomsnippet.md
Normal file
34
node-red/settings-roomsnippet.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Load room-config.js at Node-RED startup (fix "require is not defined")
|
||||
|
||||
Function nodes cannot use `require()`. Load the config in **settings.js** instead.
|
||||
|
||||
1. **On the Node-RED server**, edit:
|
||||
```bash
|
||||
nano /root/.node-red/settings.js
|
||||
```
|
||||
|
||||
2. **Find** the block that looks like:
|
||||
```javascript
|
||||
functionGlobalContext: {
|
||||
// os:require('os'),
|
||||
},
|
||||
```
|
||||
|
||||
3. **Add** the room config so it becomes:
|
||||
```javascript
|
||||
functionGlobalContext: {
|
||||
roomConfig: require('/root/.node-red/room-config.js'),
|
||||
// os:require('os'),
|
||||
},
|
||||
```
|
||||
(Use the correct path to your `room-config.js` if different.)
|
||||
|
||||
4. **Save**, then **restart Node-RED**:
|
||||
```bash
|
||||
systemctl restart node-red
|
||||
```
|
||||
or however you run Node-RED.
|
||||
|
||||
5. In your flows, **remove or disconnect** the "Load room config" Function node (or leave it unused). All nodes that use `global.get('roomConfig')` will get the config automatically.
|
||||
|
||||
**When you change room-config.js:** upload the file to the server, then restart Node-RED to reload it.
|
||||
Reference in New Issue
Block a user