Initial commit: CODESYS tree, integration docs, Node-RED scripts.
PLC application sources (NVL, rooms, boiler), HA/Node-RED integration guide, and NVL patch utilities. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
265
docs/integration-home-assistant-nodered.md
Normal file
265
docs/integration-home-assistant-nodered.md
Normal file
@@ -0,0 +1,265 @@
|
||||
# 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 `<room>_<light_num>`. The active House Lights flow also uses helpers like `input_boolean.living_room_new` and auto-provisioned `input_boolean.plc_<room>_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/<friendly_name>/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_<room>` and `light_<room>` 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_<room>` 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 |
|
||||
Reference in New Issue
Block a user