# 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. It’s 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 don’t “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 doesn’t 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. That’s a refinement of the same idea, not a different architecture. So: you don’t *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 they’re 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. := .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 | |---------|--------------------|----------|-------------| | 1–15 | 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 device’s 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 EL2809’s 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 EL2809’s **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 room’s 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 EL2809’s 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.`. The boiler’s `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..l_X_status` (and boiler if applicable). | | 5 | Pass `EtherCAT_RelayFeedback.` as `relay_status` into each `fb_room`. | So: **relay feedback = value read from the EL2809’s 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 1’s auxiliary contact.) So: **relay feedback = state of the DI that is wired to the relay’s 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. := .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. := .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