Files
kkelomatic_home/docs/codesys/plc-algorithm-design.md
nearxos 63b343f139 Update PLC algorithm documentation for EL2809 integration and relay feedback options
- Revised documentation to reflect the use of the EL2809 output module, including updated channel descriptions and process image details.
- Introduced Option C for relay feedback, clarifying the method of using output copies for feedback instead of hardware read-back.
- Enhanced comments and explanations in the main program logic to improve understanding of the relay feedback implementation across multiple rooms.

This update improves the clarity and accuracy of the documentation for the home automation system's PLC algorithms.
2026-02-07 23:02:17 +02:00

1027 lines
42 KiB
Markdown
Raw 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.

# PLC Algorithm Design - Lighting and Water Boiler Control
## Overview
This document defines the PLC algorithms for the home automation system:
1. **Lighting Control** - Improved logic with command-based control for Home Assistant and toggle-based for Zigbee switches
2. **Water Boiler Control** - Simple ON/OFF with safety features (time limit, emergency stop)
> **Naming convention**: New types, FBs, program, and NVLs use **different names** (no suffix) so the
> redesign can run alongside the existing project. Existing names (`struct_switches`, `fb_switch`, `PLC_PRG`, etc.) are left unchanged.
### Name Mapping (existing → new, no suffix)
| Existing name (do not touch) | New name | Kind |
|------------------------------|----------|------|
| `struct_switches` | `struct_room_cmds` | DUT (type) |
| `struct_lights` | `struct_room_outs` | DUT (type) |
| `fb_toogleButton` | `fb_light` | Function Block |
| `fb_switch` | `fb_room` | Function Block |
| — (new) | `fb_boiler` | Function Block |
| — (new) | `struct_boiler_cmd` | DUT (type) |
| — (new) | `struct_boiler_status` | DUT (type) |
| `PLC_PRG` | `PLC_App` | Program |
| `Lights` | (merged into `PLC_App`) | Program |
| `NVL_Sender` | `NVL_Out` | NVL |
| `NVL_Receiver` | `NVL_In` | NVL |
## System Architecture
```
┌─────────────────────────────────────────────────────────────────────────┐
│ CODESYS PLC (Raspberry Pi) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────┐ │
│ │ NVL_In │ │ MainTask │ │ NVL_Out │ │
│ │ (from Node-RED)│ │ (4ms cycle) │ │ (to Node-RED) │ │
│ └────────┬────────┘ └────────┬────────┘ └──────────┬──────────┘ │
│ │ │ │ │
│ ▼ ▼ ▲ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ PLC_App │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │ │
│ │ │ Lights │ │ Boiler │ │ Safety_Monitor │ │ │
│ │ │ (section) │ │ (section) │ │ (future) │ │ │
│ │ └──────┬──────┘ └──────┬──────┘ └─────────────────────────┘ │ │
│ └─────────┼────────────────┼──────────────────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ EtherCAT I/O │ │
│ │ ┌──────────────┐ ┌──────────────────────────────┐ │ │
│ │ │ EL1809 │ │ EL2809 │ │ │
│ │ │ 16x DI 24V │ │ 16x DO 24V (Relays) │ │ │
│ │ │ (Switches) │ │ Lights + Boiler │ │ │
│ │ └──────────────┘ └──────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Physical │ │ Physical │
│ Switches │ │ Relays │
└─────────────────┘ └─────────────────┘
```
---
## Light organization: do you need 6 per room?
### Short answers
- **Do you need exactly 6 predefined?** No. Its a convenient maximum. Unused slots are fine (no relay wired). You can use a smaller max (e.g. 4) or a larger one (e.g. 8) if you prefer.
- **Is room-based organization a good idea?** Yes. It matches how people think and how voice/HA work (“turn off bedroom”, “kitchen lights”). Keep organizing by room.
- **Best approach:** Room-based with a **fixed max lights per room** (e.g. 6 or 8). Use only the slots you need; leave the rest logically “empty.” Optionally, use **named slots** per room (e.g. `main`, `bedside_left`) instead of `l_1`..`l_6` so the PLC matches your naming and you dont “waste” slots mentally.
### Options (trade-offs)
| Approach | Description | Pros | Cons |
|---------|-------------|------|------|
| **A. Fixed N per room (current)** | Every room has e.g. 6 slots (`l_1`..`l_6`). Use only what you need. | Simple, same code for every room, easy Node-RED mapping. | Empty slots still in struct/UI; adding a 7th light in one room needs a new room or structure change. |
| **B. Named slots per room** | Each room has semantic slots: `main`, `bedside_left`, `under_cabinet`, etc. (from `light-naming-configuration.md`). Not all rooms have all slots. | Clear meaning, matches your naming; “unused” is obvious. | More types or optional slots in CODESYS; Node-RED must know which rooms have which slots. |
| **C. Flat list of lights** | One list of e.g. 64 lights; each has a relay index. Room exists only in HA/Node-RED. | No per-room limit; add lights by config. | “Turn off all kitchen” is done in HA/Node-RED (send OFF to each kitchen light). PLC doesnt do room-level all_off. |
### Recommendation
- **Stay room-based** for control and UX.
- **Keep a max per room (e.g. 6 or 8)** and accept that some slots are unused. No need to “predefine” exactly 6; think of it as “up to 6 (or 8) circuits per room.”
- If you want the PLC to reflect real names, consider **B (named slots)** later: e.g. `struct_room_lights` with `main`, `bedside_left`, `bedside_right`, `closet`, … and only wire the ones that exist in that room. Thats a refinement of the same idea, not a different architecture.
So: you dont *need* 6 predefined; you need a *maximum* per room. Organizing by room is a good idea; the rest is how many slots per room and whether theyre named or generic.
---
## Part 1: Lighting Control Algorithm
**Alignment**: This section follows the structures and logic from **`docs/redesign/fb_switch-redesign-recommendation.md`**: flat `struct_room_cmds` / `struct_room_outs`, `fb_light` with HA ON/OFF + Zigbee toggle only, and `fb_room` applying global commands by overwriting outputs.
### 1.1 Design Goals
- **Command-based control** for Home Assistant (explicit ON/OFF)
- **Toggle-based control** for Zigbee switches (edge detection)
- **State synchronization** between PLC and Home Assistant
- **Relay status feedback** for actual state monitoring
### 1.2 Data Structures (per Redesign Recommendation)
#### Input: struct_room_cmds (flat, for Node-RED/JSON compatibility)
```iec
TYPE struct_room_cmds :
STRUCT
// Home Assistant Commands (set/reset)
ha_l1_on: BOOL; // HA command: Light 1 ON
ha_l1_off: BOOL; // HA command: Light 1 OFF
ha_l2_on: BOOL;
ha_l2_off: BOOL;
ha_l3_on: BOOL;
ha_l3_off: BOOL;
ha_l4_on: BOOL;
ha_l4_off: BOOL;
ha_l5_on: BOOL;
ha_l5_off: BOOL;
ha_l6_on: BOOL;
ha_l6_off: BOOL;
// Zigbee Switch Inputs (edge detection for toggle)
zigbee_sw1: BOOL; // Zigbee switch 1 (edge detected)
zigbee_sw2: BOOL;
zigbee_sw3: BOOL;
zigbee_sw4: BOOL;
zigbee_sw5: BOOL;
zigbee_sw6: BOOL;
// Global Commands
ha_all_on: BOOL; // HA: All lights ON
ha_all_off: BOOL; // HA: All lights OFF
END_STRUCT
END_TYPE
```
#### Output: struct_room_outs (flat, control + status feedback)
```iec
TYPE struct_room_outs :
STRUCT
l_1: BOOL; // Light 1 control output
l_2: BOOL; // Light 2 control output
l_3: BOOL; // Light 3 control output
l_4: BOOL; // Light 4 control output
l_5: BOOL; // Light 5 control output
l_6: BOOL; // Light 6 control output
// Status feedback (read from actual relay outputs)
l_1_status: BOOL; // Actual relay 1 state
l_2_status: BOOL; // Actual relay 2 state
l_3_status: BOOL; // Actual relay 3 state
l_4_status: BOOL; // Actual relay 4 state
l_5_status: BOOL; // Actual relay 5 state
l_6_status: BOOL; // Actual relay 6 state
END_STRUCT
END_TYPE
```
### 1.3 Function Block: fb_light (per Redesign)
Individual light control: HA ON/OFF + Zigbee toggle only. No all_on/all_off inside this FB.
```iec
FUNCTION_BLOCK fb_light
VAR_INPUT
// Home Assistant Commands
ha_on: BOOL; // HA command: Turn ON
ha_off: BOOL; // HA command: Turn OFF
// Zigbee Switch Input
zigbee_sw: BOOL; // Zigbee switch (edge detected)
// Status Feedback
relay_status: BOOL; // Actual relay state (read from EtherCAT)
END_VAR
VAR_OUTPUT
light_output: BOOL; // Control output to relay
light_status: BOOL; // Status to send back (actual relay state)
END_VAR
VAR
// Edge triggers
r_trig_ha_on: R_TRIG;
r_trig_ha_off: R_TRIG;
r_trig_zigbee: R_TRIG;
// Internal state
light_state: BOOL := FALSE;
END_VAR
```
#### Algorithm Logic (Priority: HA OFF > HA ON > Zigbee toggle)
```iec
// =====================================================
// fb_light - Implementation (per redesign)
// =====================================================
// Priority (highest to lowest):
// 1. HA OFF command - Always turns light OFF
// 2. HA ON command - Always turns light ON
// 3. Zigbee toggle - Toggles current state
// 4. Maintain current state
// =====================================================
// Edge detection for commands
r_trig_ha_on(CLK := ha_on);
r_trig_ha_off(CLK := ha_off);
r_trig_zigbee(CLK := zigbee_sw);
// State machine
IF r_trig_ha_off.Q THEN
light_state := FALSE;
ELSIF r_trig_ha_on.Q THEN
light_state := TRUE;
ELSIF r_trig_zigbee.Q THEN
light_state := NOT light_state;
END_IF;
// Output assignment
light_output := light_state;
// Status feedback (read from actual relay)
light_status := relay_status;
```
### 1.4 Function Block: fb_room (per Redesign)
Room-level block: 6× fb_light; global commands applied by overwriting outputs.
```iec
FUNCTION_BLOCK fb_room
VAR_INPUT
switches: struct_room_cmds;
relay_status: struct_room_outs; // Read from EtherCAT outputs (actual relay states)
END_VAR
VAR_OUTPUT
lights: struct_room_outs;
END_VAR
VAR
l1: fb_light;
l2: fb_light;
l3: fb_light;
l4: fb_light;
l5: fb_light;
l6: fb_light;
END_VAR
```
#### Algorithm Logic
```iec
// =====================================================
// fb_room - Implementation (per redesign)
// =====================================================
// Light 1 Control
l1(
ha_on := switches.ha_l1_on,
ha_off := switches.ha_l1_off,
zigbee_sw := switches.zigbee_sw1,
relay_status := relay_status.l_1_status
);
lights.l_1 := l1.light_output;
lights.l_1_status := l1.light_status;
// Light 2 Control
l2(
ha_on := switches.ha_l2_on,
ha_off := switches.ha_l2_off,
zigbee_sw := switches.zigbee_sw2,
relay_status := relay_status.l_2_status
);
lights.l_2 := l2.light_output;
lights.l_2_status := l2.light_status;
// Light 3 Control
l3(
ha_on := switches.ha_l3_on,
ha_off := switches.ha_l3_off,
zigbee_sw := switches.zigbee_sw3,
relay_status := relay_status.l_3_status
);
lights.l_3 := l3.light_output;
lights.l_3_status := l3.light_status;
// Light 4 Control
l4(
ha_on := switches.ha_l4_on,
ha_off := switches.ha_l4_off,
zigbee_sw := switches.zigbee_sw4,
relay_status := relay_status.l_4_status
);
lights.l_4 := l4.light_output;
lights.l_4_status := l4.light_status;
// Light 5 Control
l5(
ha_on := switches.ha_l5_on,
ha_off := switches.ha_l5_off,
zigbee_sw := switches.zigbee_sw5,
relay_status := relay_status.l_5_status
);
lights.l_5 := l5.light_output;
lights.l_5_status := l5.light_status;
// Light 6 Control
l6(
ha_on := switches.ha_l6_on,
ha_off := switches.ha_l6_off,
zigbee_sw := switches.zigbee_sw6,
relay_status := relay_status.l_6_status
);
lights.l_6 := l6.light_output;
lights.l_6_status := l6.light_status;
// Global Commands (overwrite outputs per redesign)
IF switches.ha_all_off THEN
lights.l_1 := FALSE;
lights.l_2 := FALSE;
lights.l_3 := FALSE;
lights.l_4 := FALSE;
lights.l_5 := FALSE;
lights.l_6 := FALSE;
ELSIF switches.ha_all_on THEN
lights.l_1 := TRUE;
lights.l_2 := TRUE;
lights.l_3 := TRUE;
lights.l_4 := TRUE;
lights.l_5 := TRUE;
lights.l_6 := TRUE;
END_IF;
```
---
## Part 2: Water Boiler Control Algorithm
### 2.1 Design Goals
Based on your requirements:
- **Simple ON/OFF control** via relay output only
- **Maximum heating time limit** (safety feature)
- **Emergency stop button** support
- **State monitoring and feedback**
### 2.2 Data Structures
#### Boiler Commands (from Node-RED/Home Assistant)
```iec
TYPE struct_boiler_cmd :
STRUCT
// Control Commands
ha_on: BOOL; // Home Assistant: Turn ON
ha_off: BOOL; // Home Assistant: Turn OFF
schedule_on: BOOL; // Scheduled ON (from automation)
schedule_off: BOOL; // Scheduled OFF (from automation)
// Safety Input
emergency_stop: BOOL; // Emergency stop (physical button or HA)
// Configuration (can be set via Node-RED)
max_on_time_minutes: INT; // Maximum ON time in minutes (default: 480 = 8 hours)
END_STRUCT
END_TYPE
```
#### Boiler Status (to Node-RED/Home Assistant)
```iec
TYPE struct_boiler_status :
STRUCT
// State
state: BOOL; // Current boiler state (ON/OFF)
relay_output: BOOL; // Actual relay output state
// Runtime Info
on_time_minutes: INT; // Current ON time in minutes
remaining_minutes: INT; // Remaining time before auto-shutoff
// Safety Status
emergency_active: BOOL; // Emergency stop is active
time_limit_reached: BOOL; // Max time limit was reached
// Error Handling
error_state: BOOL; // Error detected
error_code: INT; // Error code (0=none, 1=emergency, 2=time limit)
END_STRUCT
END_TYPE
```
### 2.3 Function Block: fb_boiler
Simple ON/OFF boiler control with time limit and emergency stop.
```iec
FUNCTION_BLOCK fb_boiler
VAR_INPUT
// Control Commands
ha_on: BOOL; // Home Assistant ON command
ha_off: BOOL; // Home Assistant OFF command
schedule_on: BOOL; // Scheduled ON
schedule_off: BOOL; // Scheduled OFF
// Safety
emergency_stop: BOOL; // Emergency stop input
// Configuration
max_on_time: TIME := T#8H; // Maximum ON time (default 8 hours)
END_VAR
VAR_OUTPUT
// Outputs
relay_control: BOOL; // Control output to relay
// Status
state: BOOL; // Current state
on_time_seconds: DINT; // Current ON time in seconds
remaining_seconds: DINT; // Remaining time in seconds
// Safety Status
emergency_active: BOOL; // Emergency stop active
time_limit_reached: BOOL; // Time limit was reached
error_state: BOOL; // Error flag
error_code: INT; // Error code
END_VAR
VAR
// Internal State
internal_state: BOOL := FALSE;
last_state: BOOL := FALSE;
// Edge Detection
r_trig_ha_on: R_TRIG;
r_trig_ha_off: R_TRIG;
r_trig_schedule_on: R_TRIG;
r_trig_schedule_off: R_TRIG;
r_trig_emergency: R_TRIG;
f_trig_emergency: F_TRIG;
// Timers
on_timer: TON; // Counts ON time
// Time tracking
max_on_seconds: DINT;
END_VAR
```
### 2.4 Algorithm Logic
```iec
// =====================================================
// fb_boiler - Implementation
// =====================================================
// Priority (highest to lowest):
// 1. Emergency Stop - Immediate shutdown
// 2. Time Limit Exceeded - Auto shutdown
// 3. OFF Commands - Turn off
// 4. ON Commands - Turn on (if safe)
// =====================================================
// Calculate max time in seconds
max_on_seconds := TIME_TO_DINT(max_on_time) / 1000;
// Edge detection for commands
r_trig_ha_on(CLK := ha_on);
r_trig_ha_off(CLK := ha_off);
r_trig_schedule_on(CLK := schedule_on);
r_trig_schedule_off(CLK := schedule_off);
r_trig_emergency(CLK := emergency_stop);
f_trig_emergency(CLK := emergency_stop);
// =====================================================
// SAFETY CHECKS (highest priority)
// =====================================================
// Priority 1: Emergency Stop
IF emergency_stop THEN
internal_state := FALSE;
emergency_active := TRUE;
error_state := TRUE;
error_code := 1; // Emergency stop active
// Priority 2: Time Limit Check
ELSIF on_timer.Q THEN
internal_state := FALSE;
time_limit_reached := TRUE;
error_state := TRUE;
error_code := 2; // Time limit exceeded
// =====================================================
// CONTROL LOGIC (only when safe)
// =====================================================
ELSE
// Clear safety flags when emergency released
IF f_trig_emergency.Q THEN
emergency_active := FALSE;
error_state := FALSE;
error_code := 0;
END_IF;
// Clear time limit flag when boiler turned off
IF NOT internal_state THEN
time_limit_reached := FALSE;
IF error_code = 2 THEN
error_state := FALSE;
error_code := 0;
END_IF;
END_IF;
// Priority 3: OFF Commands (HA OFF or Schedule OFF)
IF r_trig_ha_off.Q OR r_trig_schedule_off.Q THEN
internal_state := FALSE;
// Priority 4: ON Commands (HA ON or Schedule ON)
ELSIF (r_trig_ha_on.Q OR r_trig_schedule_on.Q) AND NOT time_limit_reached THEN
internal_state := TRUE;
END_IF;
END_IF;
// =====================================================
// TIMER MANAGEMENT
// =====================================================
// ON timer - counts total ON time
on_timer(
IN := internal_state AND NOT emergency_active,
PT := max_on_time
);
// Calculate current ON time in seconds
IF internal_state THEN
on_time_seconds := TIME_TO_DINT(on_timer.ET) / 1000;
remaining_seconds := max_on_seconds - on_time_seconds;
IF remaining_seconds < 0 THEN
remaining_seconds := 0;
END_IF;
ELSE
on_time_seconds := 0;
remaining_seconds := max_on_seconds;
END_IF;
// =====================================================
// OUTPUT ASSIGNMENT
// =====================================================
// Relay control - only ON when state is ON and no emergency
relay_control := internal_state AND NOT emergency_active;
// State output
state := internal_state;
```
### 2.5 State Diagram
```
┌─────────────────────────────────────────┐
│ EMERGENCY STOP │
│ (highest priority) │
│ Sets: emergency_active = TRUE │
│ error_code = 1 │
│ relay_control = FALSE │
└─────────────────────────────────────────┘
┌──────────────────┴──────────────────┐
▼ │
┌───────────┐ │
│ │ │
START ───►│ OFF │◄──────────────────────────────┤
│ │ │
└─────┬─────┘ │
│ │
│ ON Command │
│ (ha_on OR schedule_on) │
│ AND NOT emergency_stop │
│ AND NOT time_limit_reached │
▼ │
┌───────────┐ │
│ │ Time Limit │
│ ON │─────Exceeded──────────────────┘
│ │ (on_timer.Q)
└─────┬─────┘
│ OFF Command
│ (ha_off OR schedule_off)
└──────────────────────────────────────►OFF
```
---
## Part 3: Main Program Integration
### 3.1 PLC_App structure
```iec
PROGRAM PLC_App
VAR
// =====================================================
// LIGHTING CONTROL
// =====================================================
// Room instances (fb_room)
masterBedroom: fb_room;
masterBathroom: fb_room;
bedroom_1: fb_room;
bedroom_2: fb_room;
bathroom: fb_room;
guest_wc: fb_room;
kitchen: fb_room;
pantry: fb_room;
livingRoom: fb_room;
diningRoom: fb_room;
entrance: fb_room;
hallway: fb_room;
veranda: fb_room;
front: fb_room;
back: fb_room;
side: fb_room;
// =====================================================
// WATER BOILER CONTROL
// =====================================================
boiler: fb_boiler;
// =====================================================
// GLOBAL COMMANDS
// =====================================================
global_all_lights_off: BOOL;
global_all_lights_on: BOOL;
END_VAR
```
**Option C (relay feedback):** In a GVL, define `EtherCAT_RelayFeedback` (one `struct_room_outs` per room) and `EtherCAT_Outputs` (outputs to EL2809). Initialize `EtherCAT_RelayFeedback` to zero so the first cycle has defined behavior. Each cycle, after each `fb_room` call, copy `EtherCAT_RelayFeedback.<room> := <room>.lights` (see Section 3.2).
### 3.2 Main Program Logic
```iec
// =====================================================
// PLC_App - Main Program Implementation (Option C: copy of output as feedback)
// =====================================================
// EtherCAT_RelayFeedback is filled from the OUTPUT of each room (no hardware read-back).
// Initialize EtherCAT_RelayFeedback to zero at startup so first cycle has defined behavior.
// =====================================================
// SECTION 1: LIGHTING CONTROL
// =====================================================
// Master Bedroom
masterBedroom(
switches := NVL_In.masterBedroom,
relay_status := EtherCAT_RelayFeedback.masterBedroom // Option C: previous cycle output
);
EtherCAT_RelayFeedback.masterBedroom := masterBedroom.lights; // Copy output for next cycle
NVL_Out.l_masterBedroom := masterBedroom.lights;
EtherCAT_Outputs.masterBedroom_l1 := masterBedroom.lights.l_1;
EtherCAT_Outputs.masterBedroom_l2 := masterBedroom.lights.l_2;
EtherCAT_Outputs.masterBedroom_l3 := masterBedroom.lights.l_3;
EtherCAT_Outputs.masterBedroom_l4 := masterBedroom.lights.l_4;
EtherCAT_Outputs.masterBedroom_l5 := masterBedroom.lights.l_5;
EtherCAT_Outputs.masterBedroom_l6 := masterBedroom.lights.l_6;
// Repeat same pattern for all other rooms:
// masterBathroom( switches := NVL_In.masterBathroom, relay_status := EtherCAT_RelayFeedback.masterBathroom );
// EtherCAT_RelayFeedback.masterBathroom := masterBathroom.lights;
// NVL_Out.l_masterBathroom := masterBathroom.lights;
// EtherCAT_Outputs.masterBathroom_l1 := masterBathroom.lights.l_1; ... l_2..l_6
// ... bedroom_1, bedroom_2, bathroom, guest_wc, kitchen, pantry, livingRoom, diningRoom,
// entrance, hallway, veranda, front, back, side (each: call fb_room, copy to EtherCAT_RelayFeedback, NVL_Out, EtherCAT_Outputs)
// =====================================================
// SECTION 2: WATER BOILER CONTROL
// =====================================================
boiler(
ha_on := NVL_In.boiler.ha_on,
ha_off := NVL_In.boiler.ha_off,
schedule_on := NVL_In.boiler.schedule_on,
schedule_off := NVL_In.boiler.schedule_off,
emergency_stop := NVL_In.boiler.emergency_stop
OR DI_Emergency_Stop, // Physical button OR remote
max_on_time := T#8H
);
// Map outputs
EtherCAT_Outputs.boiler_relay := boiler.relay_control;
// Status to Node-RED
NVL_Out.boiler_status.state := boiler.state;
NVL_Out.boiler_status.relay_output := boiler.relay_control;
NVL_Out.boiler_status.on_time_minutes := DINT_TO_INT(boiler.on_time_seconds / 60);
NVL_Out.boiler_status.remaining_minutes := DINT_TO_INT(boiler.remaining_seconds / 60);
NVL_Out.boiler_status.emergency_active := boiler.emergency_active;
NVL_Out.boiler_status.time_limit_reached := boiler.time_limit_reached;
NVL_Out.boiler_status.error_state := boiler.error_state;
NVL_Out.boiler_status.error_code := boiler.error_code;
```
---
## Part 4: I/O Mapping
### 4.1 EtherCAT Digital Outputs: EL2809
**Module**: Beckhoff **EL2809** 16-channel digital output, 24 V DC, 0.5 A per channel (overload/short-circuit protected).
| Channel | Address (typical) | Function | Description |
|---------|--------------------|----------|-------------|
| 115 | 16#1A00 … 16#1A0E | Lighting | Room light relays |
| 16 | 16#1A0F | Boiler | Water boiler relay |
The EL2809 has a **16-bit process image** (one word) for the output commands. In CODESYS, the EtherCAT device will expose an output variable (e.g. `EL2809_Outputs` or similar) map your lighting and boiler control bits to the corresponding channels.
### 4.2 EtherCAT Digital Inputs (if using physical emergency stop)
| Address | Function | Description |
|---------|----------|-------------|
| 16#1900 - 16#190F | Switches | Room physical switches (existing) |
| TBD | Emergency | Boiler emergency stop button (optional) |
### 4.3 How relay feedback is obtained (with EL2809)
**Chosen for this project: Option C** — relay “feedback” is a **copy of the output** we write to the EL2809. No hardware read-back; HA and the PLC stay in sync with the commanded state. Relay/wiring faults are not detected.
Relay feedback is the value passed into `fb_room` as `relay_status` and used for `l_X_status` in the status sent to Node-RED/HA. With Option C it is the same as the control output (from the previous cycle), so the UI reflects what we commanded.
**Your hardware**: **EL2809** (16-channel DO). The EL2809 drives the outputs from the process image; whether it exposes a **read-back** (TxPDO / “Input” or “Status”) depends on the EtherCAT PDO configuration. In CODESYS, after scanning the EL2809, check the devices process image for an **input** or **status** word (data from device → PLC). **EL2809 has no read-back (confirmed):** process image = 16 output bits only, no input. Use **Option C** (copy of output) or **Option B** (auxiliary contacts). Which models *do* have input? See table below.
| Model | Channels | Process image | Notes |
|-------|----------|---------------|-------|
| **EL2032** | 2 DO | 2 outputs + 2 diagnostic inputs | 24 V DC, 2 A. |
| **EL2034** | 4 DO | 4 outputs + 4 diagnostic inputs | 24 V DC, 2 A; short-circuit, line-break. |
| **EL2042** | 8 DO | 8 outputs + diagnostic inputs | 24 V DC, 0.5 A; confirm in datasheet. |
| EL2002, EL2004, EL2008 | 2/4/8 DO | Output only | No input. |
| EL2084, EL2088 | 4/8 DO | Output only | No input. |
| **EL2809** | 16 DO | **Output only** | No input (your module). |
On EL2032/EL2034 the input bits are **diagnostic** (e.g. short-circuit, line-break), not necessarily "output on" state. For true output-state read-back without wiring use Option B (auxiliary contacts) or Option C (copy of output).
There are **three practical ways** to get relay feedback:
---
#### Option A: Output read-back (only for DO modules that have Input/Status)
If the EL2809s EtherCAT configuration in CODESYS exposes an **input** (TxPDO) or **status** word from the device, that is the read-back of the output state. Use the steps and code below. **Note:** The EL2809 datasheet specifies only “16 output bits” in the process image; not all DO modules provide a separate input/read-back. In CODESYS, verify whether an **Input** (or **Status**) process image exists for the EL2809; if it does not, use Option C instead.
---
**1. EtherCAT process image (reminder)**
- **Output process image**: PLC → device. You write here to drive the EL2809 channels (your light/boiler commands).
- **Input process image**: Device → PLC. If the EL2809 supports read-back, it will send data here (e.g. actual output state or status). This is the TxPDO from the slave.
Option A uses this **input** data as relay feedback.
---
**2. Check whether the EL2809 has read-back in CODESYS**
1. Open your CODESYS project and the **Device Tree** (often under **Application** or **EtherCAT Master**).
2. Expand the EtherCAT master and locate the **EL2809** device.
3. Open the EL2809 and look at its **I/O Mapping** or **Process Image** (name depends on CODESYS version).
4. You should see at least:
- **Output**: one 16-bit word (or 16 BOOLs) this is what you write to drive the relays.
5. Check if there is also:
- **Input**: one 16-bit word (or 16 BOOLs) data *from* the EL2809.
If **Input** (or **Status** / **Actual value**) is present, the module supports read-back; use Option A. If only **Output** exists, use Option C.
---
**3. Where the variable appears**
After scanning the bus (or adding the device), CODESYS usually creates symbols for the process image, for example:
- Under a **GVL** (Global Variable List) linked to the EtherCAT device, or
- Under **I/O Mapping** as a linked variable (e.g. `EtherCAT_Master.EL2809.Input` or `EL2809_Input`).
The exact path depends on your CODESYS version and how the EtherCAT device was added. Typical patterns:
- `GVL.EtherCAT.EL2809_Input` (WORD or 16 BOOLs)
- `EtherCAT_Master.EL2809.Inputs` or `.Input`
- Or 16 separate bits: `EL2809_Input_0``EL2809_Input_15`
If no Input symbol exists, create a **linked variable**: add a new variable in a GVL, set its type to WORD (or ARRAY[0..15] OF BOOL), and link it to the EL2809s **Input** process image byte/word in the I/O mapping dialog.
---
**4. Channel-to-light mapping**
You must map each EL2809 channel to your feedback structure. Example mapping (adjust to your wiring):
| EL2809 channel | Use | Feedback target |
|----------------|-----|------------------|
| 0 (bit 0) | Light | e.g. masterBedroom l_1 |
| 1 | Light | masterBedroom l_2 |
| … | … | … |
| 14 | Light | last room / l_6 |
| 15 | Boiler | boiler relay |
Define your own table: which physical channel drives which rooms which light (and which channel is the boiler). Then map the **Input** bits to `EtherCAT_RelayFeedback` accordingly.
---
**5. Example code: fill `EtherCAT_RelayFeedback` from EL2809 Input**
Assume the EL2809s read-back is available as a **WORD** in a GVL named `GVL_IO`, with the symbol `EL2809_Input` (or as 16 BOOLs `EL2809_Input_0``EL2809_Input_15`). Adjust names to match your project.
**If you have a WORD (e.g. `GVL_IO.EL2809_Input`):**
```iec
// Map EL2809 read-back (WORD) to relay feedback. Adjust bit order and room/channel
// mapping to match your wiring. Example: Ch0..Ch5 = masterBedroom l_1..l_6.
EtherCAT_RelayFeedback.masterBedroom.l_1_status := GVL_IO.EL2809_Input.%X0; // Ch0
EtherCAT_RelayFeedback.masterBedroom.l_2_status := GVL_IO.EL2809_Input.%X1;
EtherCAT_RelayFeedback.masterBedroom.l_3_status := GVL_IO.EL2809_Input.%X2;
EtherCAT_RelayFeedback.masterBedroom.l_4_status := GVL_IO.EL2809_Input.%X3;
EtherCAT_RelayFeedback.masterBedroom.l_5_status := GVL_IO.EL2809_Input.%X4;
EtherCAT_RelayFeedback.masterBedroom.l_6_status := GVL_IO.EL2809_Input.%X5;
EtherCAT_RelayFeedback.masterBathroom.l_1_status := GVL_IO.EL2809_Input.%X6;
// ... continue for all 15 lights (channels 0..14). Channel 15 = boiler relay;
// if you want boiler relay read-back, assign to your boiler status struct instead.
```
**If you have 16 BOOLs (e.g. `GVL_IO.EL2809_Ch0` … `GVL_IO.EL2809_Ch15`):**
```iec
EtherCAT_RelayFeedback.masterBedroom.l_1_status := GVL_IO.EL2809_Ch0;
EtherCAT_RelayFeedback.masterBedroom.l_2_status := GVL_IO.EL2809_Ch1;
// ... same mapping as above, using EL2809_Ch2 .. EL2809_Ch15
```
**Where to call this:** Execute this mapping in the **same task** that runs `PLC_App`, and **before** you call `fb_room` instances (so `EtherCAT_RelayFeedback` is up to date when passed as `relay_status`). Typically this is at the top of `PLC_App` or in a dedicated “I/O update” section.
---
**6. Ensure `EtherCAT_RelayFeedback` is defined**
You need a global (or program-level) variable that holds the feedback for all rooms and the boiler, e.g.:
```iec
VAR_GLOBAL
EtherCAT_RelayFeedback: STRUCT
masterBedroom: struct_room_outs;
masterBathroom: struct_room_outs;
// ... all rooms ...
// If boiler feedback is separate: boiler_relay_status: BOOL;
END_STRUCT;
END_VAR
```
Each `fb_room` is then called with `relay_status := EtherCAT_RelayFeedback.<room>`. The boilers `relay_output` in `struct_boiler_status` can be set from the same EL2809 input bit (channel 15) if you use it for status.
---
**7. Summary Option A**
| Step | Action |
|------|--------|
| 1 | In Device Tree → EL2809, check for **Input** (or Status) in process image. |
| 2 | If present, note the symbol (e.g. `GVL_IO.EL2809_Input` or 16 BOOLs). |
| 3 | Define your channel → room/light mapping table. |
| 4 | In `PLC_App` (before calling `fb_room`), assign each Input bit to the corresponding `EtherCAT_RelayFeedback.<room>.l_X_status` (and boiler if applicable). |
| 5 | Pass `EtherCAT_RelayFeedback.<room>` as `relay_status` into each `fb_room`. |
So: **relay feedback = value read from the EL2809s Input process image**, when the device provides it.
---
#### Option B: Auxiliary contacts on the relays (hardware feedback)
The relay has (or you add) an **auxiliary (feedback) contact** that closes when the relay is energized. That contact is wired to a **digital input** on the EtherCAT bus (e.g. spare channels on the EL1809 or an extra DI module).
- **Wiring**: Relay coil ← DO channel; relay auxiliary contact → DI channel.
- **In the program**: The DI channel *is* the relay feedback.
e.g. `EtherCAT_RelayFeedback.masterBedroom.l_1_status := GVL.EtherCAT_Master.DI_Module.Ch5;`
(where Ch5 is the DI connected to relay 1s auxiliary contact.)
So: **relay feedback = state of the DI that is wired to the relays auxiliary contact.**
---
#### Option C: No hardware feedback use command as “feedback” (EL2809 without read-back)
If the EL2809 does **not** expose an input/read-back in the process image and you do **not** use auxiliary contacts (Option B), use the **output command as the “feedback”**:
- **In the program**: Build `EtherCAT_RelayFeedback` from the **same variables** you write to the EL2809 outputs. For example, after you assign `EtherCAT_Outputs.masterBedroom_l1 := masterBedroom.lights.l_1;`, also set
`EtherCAT_RelayFeedback.masterBedroom.l_1_status := masterBedroom.lights.l_1;`
(or copy from a single source of truth for each channel).
- Then `l_X_status` in `struct_room_outs` reflects “what we commanded,” not “what the relay actually did.” HA and the PLC stay in sync, but **relay or wiring faults are not detected**.
**Implementation:** Section 3.2 (PLC_App): after each `fb_room` call, set `EtherCAT_RelayFeedback.<room> := <room>.lights`; initialize `EtherCAT_RelayFeedback` to zero at startup.
---
#### Summary (with EL2809)
| Option | What you need | True feedback? | This project |
|--------|----------------|----------------|--------------|
| **A** | DO input/read-back in process image | Yes | Not used (EL2809 has no input). |
| **B** | Relay auxiliary contacts + DI | Yes | Not used. |
| **C** | Copy of output variables | No | **Used.** See Section 3.2. |
**Option C (chosen):** `fb_room` gets `relay_status` from `EtherCAT_RelayFeedback`. Each cycle, after calling `fb_room`, we set `EtherCAT_RelayFeedback.<room> := <room>.lights`, so the next cycle uses this as relay feedback. Initialize `EtherCAT_RelayFeedback` to zero at startup.
---
## Part 5: Network Variables
### 5.1 NVL_Out (PLC → Node-RED)
```iec
VAR_GLOBAL
// Lighting status (struct_room_outs per room)
l_masterBedroom: struct_room_outs;
l_masterBathroom: struct_room_outs;
l_bedroom_1: struct_room_outs;
l_bedroom_2: struct_room_outs;
l_bathroom: struct_room_outs;
l_guestWc: struct_room_outs;
l_kitchen: struct_room_outs;
l_pantry: struct_room_outs;
l_livingRoom: struct_room_outs;
l_dinningRoom: struct_room_outs;
l_entrance: struct_room_outs;
l_hallway: struct_room_outs;
l_outVeranda: struct_room_outs;
l_outFront: struct_room_outs;
l_outBack: struct_room_outs;
l_outSide: struct_room_outs;
// Boiler status
boiler_status: struct_boiler_status;
END_VAR
```
### 5.2 NVL_In (Node-RED → PLC)
```iec
VAR_GLOBAL
// Lighting commands (struct_room_cmds per room)
masterBedroom: struct_room_cmds;
masterBathroom: struct_room_cmds;
bedroom_1: struct_room_cmds;
bedroom_2: struct_room_cmds;
bathroom: struct_room_cmds;
guestWc: struct_room_cmds;
kitchen: struct_room_cmds;
pantry: struct_room_cmds;
livingRoom: struct_room_cmds;
dinningRoom: struct_room_cmds;
entrance: struct_room_cmds;
hallway: struct_room_cmds;
outVeranda: struct_room_cmds;
outFront: struct_room_cmds;
outBack: struct_room_cmds;
outSide: struct_room_cmds;
// Boiler commands
boiler: struct_boiler_cmd;
END_VAR
```
---
## Part 6: Error Codes Reference
### Boiler Error Codes
| Code | Name | Description | Action |
|------|------|-------------|--------|
| 0 | No Error | Normal operation | None |
| 1 | Emergency Stop | Emergency stop activated | Release emergency stop, then send ON command |
| 2 | Time Limit | Maximum ON time exceeded | Send OFF command, then ON again to reset |
---
## Part 7: Testing Checklist
### Lighting Tests
- [ ] HA ON command turns light ON
- [ ] HA OFF command turns light OFF
- [ ] Zigbee toggle switches light state
- [ ] All-off command turns all lights OFF
- [ ] All-on command turns all lights ON
- [ ] Relay status feedback is accurate
- [ ] Multiple commands in sequence work correctly
### Boiler Tests
- [ ] HA ON command starts boiler
- [ ] HA OFF command stops boiler
- [ ] Schedule ON command works
- [ ] Schedule OFF command works
- [ ] Emergency stop immediately stops boiler
- [ ] Emergency stop prevents ON commands
- [ ] Releasing emergency stop allows normal operation
- [ ] Time limit auto-shutoff works (test with short time)
- [ ] ON time counter is accurate
- [ ] Remaining time is accurate
- [ ] Error codes are set correctly
- [ ] Status feedback to HA is correct
---
## Implementation Notes
1. **Redesign alignment**: Lighting control (Part 1) follows **`docs/redesign/fb_switch-redesign-recommendation.md`**: flat `struct_room_cmds` / `struct_room_outs`, `fb_light` (HA ON/OFF + Zigbee toggle), `fb_room` with global commands applied by overwriting outputs. Node-RED should send `ha_l1_on`/`ha_l1_off` and `zigbee_sw1` (edge) per the redesign.
2. **Backward Compatibility**: The new lighting structure replaces the old toggle-only logic; Node-RED and HA need to be updated to the new command format.
2. **Emergency Stop**: Can be either a physical button (EtherCAT input) or a virtual command from Home Assistant/Node-RED.
3. **Time Limit Configuration**: Default is 8 hours but can be changed via `max_on_time` parameter or Node-RED configuration.
4. **Task Configuration**: Run on EtherCAT_Task (4ms cycle) for responsive control.
---
**Document Version**: 1.1
**Created**: 2026-02-07
**Updated**: Aligned Part 1 (Lighting) with `docs/redesign/fb_switch-redesign-recommendation.md`.
**Status**: Design Complete - Ready for Implementation