# Node-RED: Living Room flow This document describes the **Living Room** tab in Node-RED: how HA and Zigbee drive NVL commands and how the 328-byte buffer is built and sent to the PLC. --- ## Flow diagram ``` [Inject] ──► [trigger-state] ◄── HA entity: input_boolean.living_room_new │ ├──► [HA to NVL] ──┬──► [Build NVL_In] ──┬──► [link out 26] ──► (Flow 1: to UDP → PLC) │ │ └──► debug 39 └──► debug 36 │ │ [Zigbee2MQTT] ──► [Zigbee to NVL] ──┼──► [Build NVL_In] Living Room Door Switch │ ▲ (TS0044) ├──► [80ms delay] ──► [Clear zigbee edge] ──► [Build NVL_In] │ │ └──► debug 40 └──► debug 41 ``` - **HA path:** `trigger-state` watches `input_boolean.living_room_new` → **HA to NVL** updates `flow.nvlInState.rooms.cmd_livingroom` (ha_lN_on / ha_lN_off) → **Build NVL_In** → link out → Flow 1 → UDP to PLC. - **Zigbee path:** **zigbee2mqtt-in** (Living Room Door Switch TS0044) → **Zigbee to NVL** sets zigbee_swN in state → **Build NVL_In** (send) → then **80ms delay** → **Clear zigbee edge** (clear zigbee_swN) → **Build NVL_In** again (second packet so PLC sees one pulse). --- ## Nodes (Living Room tab) | Node | Type | Role | |------|------|------| | **trigger-state** | node-red-contrib-home-assistant-websocket | Listens to `input_boolean.living_room_new`; output 1 → HA to NVL, output 2 → blocked. | | **Inject** | inject | Manual/startup trigger into trigger-state (optional). | | **HA to NVL** | function | Maps HA entity state to `cmd_livingroom` in `flow.nvlInState`; sets ha_l1_on/ha_l1_off … ha_l6_on/ha_l6_off from entity ID and payload; then `buildAndSend: true`. | | **Zigbee to NVL** | function | Maps Zigbee action (single/double/hold/release/triple/quad) to zigbee_sw1…6; updates `flow.nvlInState.rooms.livingRoom`; sets `msg.zigbeeClear` for later clear; then `buildAndSend: true`. | | **80ms** | delay | 80 ms then passes message to Clear zigbee edge. | | **Clear zigbee edge** | function | Clears the zigbee_swN flag in `nvlInState` and sets `buildAndSend: true` again. | | **Build NVL_In** | function | Reads `flow.nvlInState`, builds 328-byte buffer (16 rooms × 20 bytes + boiler 8 bytes), sets `msg.payload = buf`; outputs to link out 26 and debug. | | **link out 26** | link out | Sends built buffer to Flow 1’s **to UDP** link in → **udp out** (10.20.30.5:1202). | | **debug 36–41** | debug | Optional debug (37 active). | --- ## Shared state: `flow.nvlInState` All rooms and boiler share one object: - **Shape:** `{ rooms: { cmd_livingroom: { ha_l1_on, ha_l1_off, … }, … }, boiler: { … } }` - **Living Room (HA test)** writes to `state.rooms.cmd_livingroom`. **Build NVL_In** reads the full object and serializes all 16 slots + boiler into the 328-byte NVL_In layout. Room names order in Build NVL_In (must match PLC NVL): `masterBedroom`, `masterBathroom`, `bedroom_1`, `bedroom_2`, `bathroom`, `guestWc`, `kitchen`, `pantry`, **`cmd_livingroom`**, `dinningRoom`, `entrance`, `hallway`, `outVeranda`, `outFront`, `outBack`, `outSide`. --- ## Test variable: `cmd_livingroom` The flow uses **`cmd_livingroom`** (the NVL variable you added for testing), not `livingRoom`. HA to NVL writes to `flow.nvlInState.rooms.cmd_livingroom`, and Build NVL_In reads that key for the living-room slot in the 328-byte buffer (same 20-byte layout as struct_room_cmds). --- ## What the trigger-state must output to turn the light ON (PLC) The **HA to NVL** node decides ON vs OFF from the message it receives. For the PLC to turn the light **on**: | Field | Value | |----------|--------| | **payload** | `'on'` (string) **or** `true` (boolean) | | **topic** | Entity ID (e.g. `input_boolean.living_room_new`). Optional for which light 1–6: if the last part after `_` is a number (e.g. `living_room_2` → 2), that light is used; otherwise light **1** is used. | So the **trigger-state** should output to the first (allowed) output: - **Turn ON:** `msg.payload === 'on'` or `msg.payload === true` → PLC gets `ha_lN_on = true` in **cmd_livingroom**. - **Turn OFF:** any other payload (e.g. `'off'`, `false`) → PLC gets `ha_lN_off = true` in **cmd_livingroom**. With the current entity `input_boolean.living_room_new` there is no `_1`…`_6`, so it always controls **light 1** in the living room. --- ## HA to NVL (function code) - **Room (state key):** `cmd_livingroom` (NVL variable for testing). - **Entity ID:** from `msg.topic` (e.g. `input_boolean.living_room_new`). Light index 1–6 is parsed from the last `_N` in the entity ID; default 1. - **ON/OFF:** `msg.payload === 'on'` or `true` → set `ha_lN_on = true`, else `ha_lN_off = true` (the other cleared). - Then sets `msg.payload = { buildAndSend: true }` and returns. ### Debug logs (in Node-RED Debug sidebar) The function uses `node.warn()` so messages appear in the **Debug** tab (with the warning icon) without needing a separate debug node: 1. **On input:** `[HA to NVL] topic=... payload=... (type ...) → lightNum=... isOn=...` Use this to confirm what the trigger-state sent: `topic`, `payload`, its type, and the parsed `lightNum` (1–6) and `isOn` (true/false). 2. **After state update:** `[HA to NVL] set cmd_livingroom ha_lN_on=true, buildAndSend` (or `ha_lN_off`). Confirms which command was written and that `buildAndSend` is being passed on. If `isOn` is false when you expect ON, check that the trigger-state output is exactly `'on'` (string) or `true` (boolean). If `topic` is wrong or missing, the light number may default to 1. **HA to NVL function with debug:** full code in [ha-to-nvl-function.js](ha-to-nvl-function.js) (uses **cmd_livingroom**). Copy that file’s contents into the Node-RED function node. --- ## 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. --- ## Build NVL_In (function code) - **Buffer:** 328 bytes (Buffer.alloc(328)). - **Rooms:** 16 rooms × 20 bytes each: bytes 0–11 = ha_l1_on, ha_l1_off, … ha_l6_off; bytes 12–17 = zigbee_sw1…6; bytes 18–19 = ha_all_on, ha_all_off. - **Boiler:** offset 320: ha_on, ha_off, schedule_on, schedule_off, emergency_stop, (reserved), max_on_time_minutes (INT16 LE). - **Output:** `msg.payload = buf`; wired to **link out 26** (and debug). --- ## Zigbee device - **Friendly name:** Living Room Door Switch (TS0044) - **Device ID:** 0xa4c1383d7921827a - **Server:** zigbee2mqtt-server config (id 4e20fc347c658518) --- ## Do you need Build NVL_In? **No, if you use the nvl-send node.** The nvl-send node (node-red-contrib-nvl) does the binary packing from a payload object. So you can: 1. **Remove** the **Build NVL_In** node and the **link out 26** (and the link in “to UDP” path that receives it). 2. **Add** a single function that builds the payload object from `flow.nvlInState` and feed that into **nvl-send** → **udp out**. Code for that function (copy into a function node): **[state-to-nvl-send-payload.js](state-to-nvl-send-payload.js)**. It builds the object with the 15 `struct_switches` rooms (default false) and **cmd_livingroom** from `state.rooms.cmd_livingroom`. Wire: **HA to NVL / Zigbee to NVL** → **state-to-nvl-send-payload** → **nvl-send** → **udp out**. --- ## Syncing PLC state back to Home Assistant (Zigbee / any source → HA UI) When you change a light with the **Zigbee switch** (or any source that doesn’t go through HA), the PLC updates but HA doesn’t, so entities can stay out of sync. To keep HA in sync for **all** mapped lights: 1. **After the nvl-receive node** (Flow 1), add a **function** node and paste the code from **[nvl-to-ha-sync-livingroom.js](nvl-to-ha-sync-livingroom.js)**. 2. In that function, **`LIGHT_ENTITY_MAP`** defines which PLC room/light → HA entity to sync. Each entry is `{ room: 'payloadKey', light: 1..6, entityId: 'domain.entity_id' }`. Add one row per light (any room/zone). Example: - `{ room: 'light_livingRoom', light: 1, entityId: 'input_boolean.living_room_new' }` - `{ room: 'l_kitchen', light: 1, entityId: 'light.kitchen_ceiling' }` 3. Give the function **2 outputs**. Then either: **Option A – Action node:** In the palette look under **Home Assistant** for **Action** ([docs](https://zachowj.github.io/node-red-contrib-home-assistant-websocket/node/action.html)). The sync already sends `payload.action` (e.g. `input_boolean.turn_on`) and `payload.target.entity_id` (array). Output 1 → **Action** (turn_on), Output 2 → **Action** (turn_off). You can leave Action config empty and use “Block Input Overrides” off so `msg.payload` sets the action and target. **Option B – HTTP:** Use **[nvl-to-ha-http-call.js](nvl-to-ha-http-call.js)** + **http request** node; set `flow.set('ha_base_url', 'http://HA_IP:8123')` and `flow.set('ha_token', 'YOUR_TOKEN')`. 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) - **Option A (with Build NVL_In):** Living Room builds the buffer in a function and sends it via **link out 26** → Flow 1 **link in "to UDP"** → **udp out**. - **Option B (with nvl-send only):** HA to NVL / Zigbee to NVL → **state-to-nvl-send-payload** → **nvl-send** → **udp out**. No Build NVL_In, no link nodes needed for this path. - The PLC (CODESYS) receives NVL_In on port 1202 and drives lights/boiler from the packed struct. --- ## File reference - **Flows export:** [nodered-flows.json](nodered-flows.json) — Living Room tab id: `7de41d810b04d992` - **Payload layout (PLC/Node-RED):** [codesys/src/NVL/nodered-payload.md](../../codesys/src/NVL/nodered-payload.md)