diff --git a/docs/integration/all-lights-and-rooms.md b/docs/integration/all-lights-and-rooms.md index 338106e..5e98a74 100644 --- a/docs/integration/all-lights-and-rooms.md +++ b/docs/integration/all-lights-and-rooms.md @@ -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` 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. | diff --git a/docs/integration/clear-zigbee-edge.js b/docs/integration/clear-zigbee-edge.js new file mode 100644 index 0000000..cd03ce0 --- /dev/null +++ b/docs/integration/clear-zigbee-edge.js @@ -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; diff --git a/docs/integration/nodered-flow-check-report.md b/docs/integration/nodered-flow-check-report.md new file mode 100644 index 0000000..898de27 --- /dev/null +++ b/docs/integration/nodered-flow-check-report.md @@ -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). diff --git a/docs/integration/nodered-livingroom-flow.md b/docs/integration/nodered-livingroom-flow.md index 9bff290..2a50387 100644 --- a/docs/integration/nodered-livingroom-flow.md +++ b/docs/integration/nodered-livingroom-flow.md @@ -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). --- diff --git a/docs/integration/zigbee-to-nvl-function.js b/docs/integration/zigbee-to-nvl-function.js index bb8df3a..94506f5 100644 --- a/docs/integration/zigbee-to-nvl-function.js +++ b/docs/integration/zigbee-to-nvl-function.js @@ -1,35 +1,108 @@ -// Writes to state.rooms.. 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; diff --git a/node-red/reload-room-config-docs.md b/node-red/reload-room-config-docs.md new file mode 100644 index 0000000..5a94678 --- /dev/null +++ b/node-red/reload-room-config-docs.md @@ -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. diff --git a/node-red/reload-room-config-function.js b/node-red/reload-room-config-function.js new file mode 100644 index 0000000..c02ba72 --- /dev/null +++ b/node-red/reload-room-config-function.js @@ -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; +} diff --git a/node-red/room-config-loader.js b/node-red/room-config-loader.js index 17eda52..5a281b3 100644 --- a/node-red/room-config-loader.js +++ b/node-red/room-config-loader.js @@ -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; diff --git a/node-red/room-config.js b/node-red/room-config.js index 5d5098d..7921912 100644 --- a/node-red/room-config.js +++ b/node-red/room-config.js @@ -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) + }, }, }; diff --git a/node-red/settings-roomsnippet.md b/node-red/settings-roomsnippet.md new file mode 100644 index 0000000..19f0285 --- /dev/null +++ b/node-red/settings-roomsnippet.md @@ -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.