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

12 KiB
Raw Permalink Blame History

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.

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-sendudp out Commands: HA, Zigbee, schedules
NVL_Sender Outbound from PLC udp innvl-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_onha_l6_on, ha_l1_offha_l6_off fb_light via R_TRIG (rising edge only)
Zigbee pulses zigbee_sw1zigbee_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_1l_6 Commanded relay state (written to NVL before EtherCAT mapping)
l_1_statusl_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_cmdfb_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 NVLserver-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 out10.77.70.5:1202.

  5. Return path: udp in :1202nvl-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:

{ 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.yamldefault_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_statusinput_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

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