# CODESYS ↔ Node-RED ↔ Home Assistant integration **Documented:** 2026-05-24 **Sources:** Live config on `root@10.77.50.5` (`/srv/docker-volumes/…`) and local `codesys-tree/` mirror. **CODESYS Phase 1 project (copy, does not overwrite original):** `C:\codesys-projects\Codesys_Home_Automation\Home_Automation_Boiler_Phase1\Home_Automation.project` Original remains: `C:\codesys-projects\Codesys_Home_Automation\Home_Automation.project` This stack runs home automation logic on a **CODESYS PLC** (EtherCAT relays, room function blocks). **Node-RED** is the protocol bridge: it maps Home Assistant and Zigbee events into PLC Network Variables (NVL) over **UDP**, and mirrors PLC relay state back into HA. **Home Assistant** is the UI, automations, and helper entities—not the real-time I/O path to the PLC. --- ## Infrastructure overview | Role | Host / container | VLAN70 IP | Notes | |------|------------------|-----------|--------| | Docker host (mgmt) | Proxmox CT, SSH `root@10.77.50.5` | `10.77.50.5` | Runs `home_infra` compose; macvlan services live on VLAN70 | | Mosquitto | `mqtt` | `10.77.70.6` | Port `1883`, auth required (`allow_anonymous false`) | | Home Assistant | `homeassistant` | `10.77.70.11` | Config: `/srv/docker-volumes/config/homeassistant/config` | | Node-RED | `nodered` | `10.77.70.12` | Data: `/srv/docker-volumes/config/nodered/data` | | Zigbee2MQTT | `zigbee2mqtt` | `10.77.70.13` | MQTT `mqtt://10.77.70.6`, base topic `zigbee2mqtt` | | **CODESYS PLC** | (device, not in this compose) | **`10.77.70.5`** | NVL UDP port **`1202`** | Compose file: `/srv/docker-volumes/stacks/home_infra/docker-compose.yml`. ```mermaid flowchart LR subgraph HA["Home Assistant 10.77.70.11"] UI[Dashboard / automations] Helpers[input_boolean helpers] end subgraph NR["Node-RED 10.77.70.12"] HA2NVL[HA to NVL] Z2NVL[Zigbee to NVL] NVL2HA[NVL to HA Sync] NVLSend[nvl-send] NVLRecv[nvl-receive] end subgraph MQTT["Mosquitto 10.77.70.6"] end subgraph Z2M["Zigbee2MQTT 10.77.70.13"] end subgraph PLC["CODESYS PLC 10.77.70.5"] RecvGVL[NVL_Receiver] SendGVL[NVL_Sender] FB[fb_light / fb_room / fb_boiler] IO[EtherCAT K1–K4] end UI --> Helpers Helpers <-->|WebSocket API| HA2NVL Helpers <-->|WebSocket API| NVL2HA Z2M --> MQTT MQTT --> NR Z2M --> Z2NVL HA2NVL --> NVLSend Z2NVL --> NVLSend NVLSend -->|UDP :1202| RecvGVL SendGVL -->|UDP :1202| NVLRecv NVLRecv --> NVL2HA RecvGVL --> FB --> IO IO --> SendGVL ``` --- ## CODESYS side (this repository) ### Network Variable Lists (UDP) | GVL | Direction (from PLC view) | Node-RED node | Purpose | |-----|---------------------------|---------------|---------| | `NVL_Receiver` | **Inbound** to PLC | `nvl-send` → `udp out` | Commands: HA, Zigbee, schedules | | `NVL_Sender` | **Outbound** from PLC | `udp in` → `nvl-receive` | Status: relay commands + feedback | `NVL_Receiver.st` is documented as received via **UDP**. Node-RED sends to **`10.77.70.5:1202`**; Node-RED listens on **`1202`** for PLC telegrams. ### Key structures **`struct_room_cmds`** (in `NVL_Receiver` as `cmd_*` rooms)—one instance per room, e.g. `cmd_livingRoom`: | Field group | Members | Used by | |-------------|---------|---------| | HA pulses | `ha_l1_on` … `ha_l6_on`, `ha_l1_off` … `ha_l6_off` | `fb_light` via `R_TRIG` (rising edge only) | | Zigbee pulses | `zigbee_sw1` … `zigbee_sw6` | `fb_light` toggle on edge | | Room-wide | `ha_all_on`, `ha_all_off` | Room-level FBs | **`struct_room_outs`** (in `NVL_Sender` as `light_*` rooms)—per-room outputs: | Field | Meaning | |-------|---------| | `l_1` … `l_6` | Commanded relay state (written to NVL before EtherCAT mapping) | | `l_1_status` … `l_6_status` | Actual relay feedback | Physical mapping: see `codesys-tree/_io.md` (EtherCAT `%QX*` → `Application.NVL_Sender.light_*`). **Legacy / parallel structs** in `NVL_Sender` / `NVL_Receiver`: `struct_lights` (`l_*` rooms) and `struct_switches` (`sw_*`, `all_on`/`all_off`)—still present in GVLs; the active redesign path for rooms is `cmd_*` + `light_*` + `fb_light`. **Boiler (Phase 1 on PLC):** `NVL_Receiver.boiler_cmd` → `fb_boiler` in `Boiler_PRG`; status on `NVL_Sender.boiler_status`; **coil** on `boiler_status.relay_output` (`%QX5.4` on K4 ch 13) — **not** `light_shower.l_6`. Node-RED must use **`boiler_cmd`** / **`boiler_status`** (Phase 2); legacy `cmd_shower.ha_l6_*` no longer drives the heater on the PLC. ### Logic priority (`fb_light`) 1. HA OFF (`ha_off` / `ha_lN_off` pulse) 2. HA ON (`ha_on` / `ha_lN_on` pulse) 3. Zigbee toggle (`zigbee_sw` pulse) 4. Hold state Outputs feed `NVL_Sender`; EtherCAT maps those BOOLs to contactors. --- ## Node-RED side ### Main tabs (flows) | Tab | Role | |-----|------| | **House Lights (PLC)** | Production path: NVL UDP, HA sync, Zigbee dispatch, `room-config.js` | | **Zigbee** | MQTT subscription to `zigbee2mqtt/#` (switches; some direct light control) | | **Water heater** | Boiler HA entities, timers, safety shutoff, bridge to `cmd_shower` | | **PLC Troubleshoot (cmd_*)** | Dashboard UI to pulse any `cmd_*` field for testing | | Flow 7 | Disabled (legacy) | ### NVL pipeline (House Lights tab) 1. **Inputs** accumulate into `flow.nvlInState` (`{ rooms: {}, boiler: {} }`): - **HA to NVL** — `server-state-changed` on helpers matching `plc_*`, `living_room`, etc.; sets `ha_lN_on` or `ha_lN_off` for the mapped `cmd_*` room. - **Zigbee to NVL** — MQTT actions → `zigbee_swN` pulse on the bound `cmd_*` room. - **Zigbee switch dispatch** — routes each button to PLC, direct Zigbee2MQTT light, or HA `switch.*` per `switchBindings` in `room-config.js`. 2. **STATE TO NVL** — calls `roomConfigLib.buildNvlSendPayload()` with all `roomNames` from config. 3. **`nvl-send`** — serializes GVL matching **`NVL_Receiver`** (see node `definition` in flows; aligns with `NVL_Receiver.st` in the tree). 4. **`udp out`** → `10.77.70.5:1202`. 5. **Return path:** **`udp in`** `:1202` → **`nvl-receive`** (parses **`NVL_Sender`**) → **NVL to HA Sync** → Home Assistant `api-call-service` (`turn_on` / `turn_off` on mapped helpers). 6. **Edge clearing** — after ~80 ms, **Clear NVL edges** / **Clear zigbee edge** reset pulse bits so the PLC sees rising edges on the next command (matches `R_TRIG` in ST). ### Configuration files (single source of truth) | File | Purpose | |------|---------| | `/data/room-config.js` | Room list, `plcRoomToCmd`, `lightEntityMap`, `switchBindings`, Zigbee `deviceIdToName` | | `/data/room-config-lib.js` | `buildNvlSendPayload`, HA slug/entity helpers, Zigbee action parsing, switch dispatch | | `settings.js` | Loads `room-config.js` at startup; exposes `global.roomConfig` / `global.roomConfigLib` | `roomNames` in `room-config.js` must match CODESYS `cmd_*` variables in `NVL_Receiver.st`. Example mapping entry: ```javascript { room: 'light_livingRoom', light: 1, cmdRoom: 'cmd_livingRoom', entityId: 'input_boolean.living_room_new', name: 'Living Room Main' } ``` - `room` — key in **NVL_Sender** payload (`light_*`). - `cmdRoom` — key in **NVL_Receiver** payload (`cmd_*`). - `entityId` — HA helper updated on PLC state changes. --- ## Home Assistant side ### Core config - `configuration.yaml` — `default_config`, reverse proxy trust for `10.77.70.1` / `10.77.10.1`, packages. - **MQTT** integration — broker `10.77.70.6:1883` (credentials in `.storage`, not in git). - **Node-RED** custom integration (`custom_components/nodered`) — links HA events to Node-RED flows. - Public URL: `ha.paraskeva.net` (via OPNsense Caddy). ### PLC light helpers Package `packages/lights_by_room.yaml` defines `input_boolean` helpers; naming convention `_`. The active House Lights flow also uses helpers like `input_boolean.living_room_new` and auto-provisioned `input_boolean.plc__lN` from `lightEntityMap` (via **Create HA helper** nodes on boot). Helpers are **mirrors** of PLC state, not the authority for relay coils—the PLC is. ### Echo suppression When Node-RED writes a helper from NVL→HA, it sets a short **`haEchoSuppress`** window so the resulting HA state-change event does not loop back through HA→NVL. --- ## Zigbee path 1. Switches publish to `zigbee2mqtt//action` (and related topics). 2. Node-RED **zigbee2mqtt-in** nodes receive events. 3. **Zigbee switch dispatch** looks up `switchBindings[device][button]`: - **PLC:** `{ room: 'cmd_hallway', light: 2 }` → `zigbee_sw2` pulse. - **Direct Z2M light:** `{ z2m: { device: 'Master Bedroom Spots', command: 'state', payload: 'toggle' } }`. - **HA switch:** `{ ha: { entity_id: 'switch.xxx', service: 'toggle' } }`. Coordinator: SLZB-06 (`10.77.70.60`) — separate from the PLC path. --- ## Water heater / boiler | Layer | Mechanism | |-------|-----------| | HA | `Water_boiler` entities, timer remaining sensor, automations in **Water heater** tab | | Node-RED | **Boiler → boiler_cmd pulse** → NVL; feedback from **`boiler_status`** → `input_boolean.water_boiler` | | PLC | `fb_boiler` / `struct_boiler_cmd` (full boiler FB exists; shower light channel used as integration shortcut) | | Safety | Startup **Safety Check** turns boiler off if PLC reports ON; **3-Hour Safety Timer** in Node-RED | Boiler command fields in `struct_boiler_cmd` (`ha_on`, `ha_off`, `schedule_*`, `emergency_stop`, `max_on_time_minutes`) are also packed in legacy **Build NVL_In** buffer logic for older flows; the current Water heater tab primarily uses the `cmd_shower` / `light_shower.l_6` bridge. --- ## MQTT vs UDP (when each is used) | Transport | Between | Data | |-----------|---------|------| | **UDP :1202** | Node-RED ↔ PLC | Binary NVL (GVL mirror)—**time-critical I/O** | | **MQTT** | Zigbee2MQTT ↔ Node-RED; HA ↔ MQTT devices | Zigbee events; non-PLC devices; optional HA entities | | **HA WebSocket** | Node-RED ↔ Home Assistant | Helper state, services, entity registry | The PLC does **not** speak MQTT in this architecture. --- ## Adding a new PLC-controlled light 1. **CODESYS:** Ensure `cmd_` and `light_` exist in `NVL_Receiver` / `NVL_Sender`; wire `fb_light` / `fb_room`; map `NVL_Sender.light_* .l_N` in device IO (`_io.md` / `write_io_mapping`). 2. **Node-RED:** Add entry to `lightEntityMap` and, if needed, `switchBindings` in `room-config.js`; confirm `cmd_` is in `roomNames`. 3. **Home Assistant:** Deploy helper (manual YAML package or let Node-RED **Create HA helper** on boot). 4. **Test:** Use **PLC Troubleshoot** tab to pulse `ha_l1_on`, then confirm NVL→HA sync updates the helper. --- ## Operational notes - **Re-sync local tree:** After CODESYS changes on the Windows VM, run `read_project_inventory` and refresh `codesys-tree/` (see `codesys-tree/README.md`). - **Node-RED backups:** Multiple `flows.json.bak-*` files exist under `/data`; production tab is **House Lights (PLC)**. - **Known IO issue:** `light_livingRoom.l_1` is mapped to two EtherCAT outputs (K2 ch 10 and K4 ch 16)—see `_io.md`. - **Secrets:** MQTT and HA tokens live in Docker volumes / HA `.storage`; do not commit them to this repo. --- ## Quick reference: variable name alignment | Concept | CODESYS (`NVL_Receiver`) | CODESYS (`NVL_Sender`) | Node-RED `nvl-send` / `nvl-receive` | |---------|--------------------------|------------------------|-------------------------------------| | Living room commands | `cmd_livingRoom` : `struct_room_cmds` | — | `payload.cmd_livingRoom` | | Living room outputs | — | `light_livingRoom` : `struct_room_outs` | `payload.light_livingRoom` | | Hallway switch → PLC | `cmd_hallway.zigbee_sw*` | — | Via `switchBindings` | | HA toggle living room 1 | `cmd_livingRoom.ha_l1_on/off` | — | From `input_boolean.living_room_new` | --- ## Related files in this repo | Path | Content | |------|---------| | `codesys-tree/Device/.../NVL_Receiver.st` | Inbound GVL (commands) | | `codesys-tree/Device/.../NVL_Sender.st` | Outbound GVL (status + coil image) | | `codesys-tree/Device/.../struct_room_cmds.st` | HA/Zigbee command struct | | `codesys-tree/Device/.../struct_room_outs.st` | Per-light outputs + status | | `codesys-tree/Device/.../fb_light.st` | Single-light logic | | `codesys-tree/_io.md` | EtherCAT ↔ `NVL_Sender` mapping |