Files
codesys-home-automation/docs/integration-home-assistant-nodered.md
nearxos d98c44bfa3 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>
2026-05-26 21:21:56 +03:00

266 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 K1K4]
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 |