# 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 – light state only)

With Option C (no relay read-back), one set of values is enough: we send `l_1..l_6` to the relay and to Node-RED as "light status".

```iec
TYPE struct_room_outs :
STRUCT
    l_1: BOOL;  // Light 1
    l_2: BOOL;  // Light 2
    l_3: BOOL;  // Light 3
    l_4: BOOL;  // Light 4
    l_5: BOOL;  // Light 5
    l_6: BOOL;  // Light 6
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..6 (relay_status = previous cycle output for Option C)
l1(ha_on := switches.ha_l1_on, ha_off := switches.ha_l1_off, zigbee_sw := switches.zigbee_sw1, relay_status := relay_status.l_1);
lights.l_1 := l1.light_output;

l2(ha_on := switches.ha_l2_on, ha_off := switches.ha_l2_off, zigbee_sw := switches.zigbee_sw2, relay_status := relay_status.l_2);
lights.l_2 := l2.light_output;

l3(ha_on := switches.ha_l3_on, ha_off := switches.ha_l3_off, zigbee_sw := switches.zigbee_sw3, relay_status := relay_status.l_3);
lights.l_3 := l3.light_output;

l4(ha_on := switches.ha_l4_on, ha_off := switches.ha_l4_off, zigbee_sw := switches.zigbee_sw4, relay_status := relay_status.l_4);
lights.l_4 := l4.light_output;

l5(ha_on := switches.ha_l5_on, ha_off := switches.ha_l5_off, zigbee_sw := switches.zigbee_sw5, relay_status := relay_status.l_5);
lights.l_5 := l5.light_output;

l6(ha_on := switches.ha_l6_on, ha_off := switches.ha_l6_off, zigbee_sw := switches.zigbee_sw6, relay_status := relay_status.l_6);
lights.l_6 := l6.light_output;

// 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.0 Where to declare EtherCAT_RelayFeedback (and optional EtherCAT_Outputs)

You only need **EtherCAT_RelayFeedback** in a GVL for Option C (previous-cycle output). For the EL2809 DO channels you can **map directly in the device tree** to the variables you already write: e.g. link Ch0..Ch5 to `NVL_Out.l_masterBedroom.l_1` … `l_6`, Ch6..Ch11 to `NVL_Out.l_masterBathroom.l_1` … `l_6`, and Ch15 to `NVL_Out.boiler_status.relay_output` (or a GVL BOOL). Then no **EtherCAT_Outputs** GVL or assignments in `PLC_App` are needed.

1. In CODESYS, add or open a **Global Variable List** (e.g. `GVL_IO`).
2. Declare **EtherCAT_RelayFeedback** (one `struct_room_outs` per room) as below. Optionally add **EtherCAT_Outputs** (or a WORD) only if you prefer not to map the EL2809 directly to NVL_Out.

**Example GVL (e.g. `GVL_IO`):**

```iec
VAR_GLOBAL
    // ---------- Option C: relay feedback (copy of output) ----------
    EtherCAT_RelayFeedback: STRUCT
        masterBedroom: struct_room_outs;
        masterBathroom: struct_room_outs;
        bedroom_1: struct_room_outs;
        bedroom_2: struct_room_outs;
        bathroom: struct_room_outs;
        guest_wc: struct_room_outs;
        kitchen: struct_room_outs;
        pantry: struct_room_outs;
        livingRoom: struct_room_outs;
        diningRoom: struct_room_outs;
        entrance: struct_room_outs;
        hallway: struct_room_outs;
        veranda: struct_room_outs;
        front: struct_room_outs;
        back: struct_room_outs;
        side: struct_room_outs;
    END_STRUCT;

    // ---------- Outputs to EL2809 (15 lights + 1 boiler) ----------
    EtherCAT_Outputs: STRUCT
        masterBedroom_l1: BOOL;
        masterBedroom_l2: BOOL;
        masterBedroom_l3: BOOL;
        masterBedroom_l4: BOOL;
        masterBedroom_l5: BOOL;
        masterBedroom_l6: BOOL;
        masterBathroom_l1: BOOL;
        masterBathroom_l2: BOOL;
        masterBathroom_l3: BOOL;
        masterBathroom_l4: BOOL;
        masterBathroom_l5: BOOL;
        masterBathroom_l6: BOOL;
        bedroom_1_l1: BOOL;
        bedroom_1_l2: BOOL;
        bedroom_1_l3: BOOL;
        bedroom_1_l4: BOOL;
        bedroom_1_l5: BOOL;
        bedroom_1_l6: BOOL;
        bedroom_2_l1: BOOL;
        bedroom_2_l2: BOOL;
        bedroom_2_l3: BOOL;
        bedroom_2_l4: BOOL;
        bedroom_2_l5: BOOL;
        bedroom_2_l6: BOOL;
        bathroom_l1: BOOL;
        bathroom_l2: BOOL;
        bathroom_l3: BOOL;
        bathroom_l4: BOOL;
        bathroom_l5: BOOL;
        bathroom_l6: BOOL;
        guest_wc_l1: BOOL;
        guest_wc_l2: BOOL;
        guest_wc_l3: BOOL;
        guest_wc_l4: BOOL;
        guest_wc_l5: BOOL;
        guest_wc_l6: BOOL;
        kitchen_l1: BOOL;
        kitchen_l2: BOOL;
        kitchen_l3: BOOL;
        kitchen_l4: BOOL;
        kitchen_l5: BOOL;
        kitchen_l6: BOOL;
        pantry_l1: BOOL;
        pantry_l2: BOOL;
        pantry_l3: BOOL;
        pantry_l4: BOOL;
        pantry_l5: BOOL;
        pantry_l6: BOOL;
        livingRoom_l1: BOOL;
        livingRoom_l2: BOOL;
        livingRoom_l3: BOOL;
        livingRoom_l4: BOOL;
        livingRoom_l5: BOOL;
        livingRoom_l6: BOOL;
        diningRoom_l1: BOOL;
        diningRoom_l2: BOOL;
        diningRoom_l3: BOOL;
        diningRoom_l4: BOOL;
        diningRoom_l5: BOOL;
        diningRoom_l6: BOOL;
        entrance_l1: BOOL;
        entrance_l2: BOOL;
        entrance_l3: BOOL;
        entrance_l4: BOOL;
        entrance_l5: BOOL;
        entrance_l6: BOOL;
        hallway_l1: BOOL;
        hallway_l2: BOOL;
        hallway_l3: BOOL;
        hallway_l4: BOOL;
        hallway_l5: BOOL;
        hallway_l6: BOOL;
        veranda_l1: BOOL;
        veranda_l2: BOOL;
        veranda_l3: BOOL;
        veranda_l4: BOOL;
        veranda_l5: BOOL;
        veranda_l6: BOOL;
        front_l1: BOOL;
        front_l2: BOOL;
        front_l3: BOOL;
        front_l4: BOOL;
        front_l5: BOOL;
        front_l6: BOOL;
        back_l1: BOOL;
        back_l2: BOOL;
        back_l3: BOOL;
        back_l4: BOOL;
        back_l5: BOOL;
        back_l6: BOOL;
        side_l1: BOOL;
        side_l2: BOOL;
        side_l3: BOOL;
        side_l4: BOOL;
        side_l5: BOOL;
        side_l6: BOOL;
        boiler_relay: BOOL;
    END_STRUCT;

    // ---------- Optional: EL2809 output word (link this to the device in I/O mapping) ----------
    // EL2809_Output: WORD;  // Then in PLC_App assign bits from EtherCAT_Outputs to EL2809_Output
END_VAR
```

3. **Initialization:** In the GVL properties, set **"Variable Initialization"** so that `EtherCAT_RelayFeedback` is initialized (e.g. CODESYS initializes STRUCT to zero by default). Or in the first scan of `PLC_App` you can clear it once.

4. **Linking to the EL2809:** In the **Device Tree**, open the EtherCAT master → EL2809 → **I/O Mapping** (or **Process Image**). Link each EL2809 **Output** channel directly to the variable you write (e.g. Ch0 → `NVL_Out.l_masterBedroom.l_1`, Ch1 → `l_2`, … Ch15 → boiler relay). Then `PLC_App` only assigns `NVL_Out.l_* := <room>.lights` and boiler status; no separate output struct needed. Alternatively, link the 16 bits to a **WORD** or 16 BOOLs that you assign from room lights and boiler in `PLC_App` (e.g. an optional `EtherCAT_Outputs` GVL).

So: **EtherCAT_RelayFeedback** is in a **GVL** for Option C. The EL2809 output is linked either **directly to NVL_Out** (and boiler) or to an optional output GVL that you fill in `PLC_App`.

### 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). Initialize it to zero so the first cycle has defined behavior. Each cycle, after each `fb_room` call, copy `EtherCAT_RelayFeedback.<room> := <room>.lights`. Map the EL2809 DO channels in the device tree directly to the variables you write (e.g. `NVL_Out.l_*` and boiler relay); a separate `EtherCAT_Outputs` GVL is not required (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` (same struct as output: `l_1..l_6`) and sent to Node-RED/HA as light status. 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 := GVL_IO.EL2809_Input.%X0;  // Ch0
EtherCAT_RelayFeedback.masterBedroom.l_2 := GVL_IO.EL2809_Input.%X1;
EtherCAT_RelayFeedback.masterBedroom.l_3 := GVL_IO.EL2809_Input.%X2;
EtherCAT_RelayFeedback.masterBedroom.l_4 := GVL_IO.EL2809_Input.%X3;
EtherCAT_RelayFeedback.masterBedroom.l_5 := GVL_IO.EL2809_Input.%X4;
EtherCAT_RelayFeedback.masterBedroom.l_6 := GVL_IO.EL2809_Input.%X5;
EtherCAT_RelayFeedback.masterBathroom.l_1 := 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 := GVL_IO.EL2809_Ch0;
EtherCAT_RelayFeedback.masterBedroom.l_2 := 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 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.<room>.l_X` (and boiler if applicable). |
| 5 | Pass `EtherCAT_RelayFeedback.<room>` 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 := 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**: After each `fb_room` call, set `EtherCAT_RelayFeedback.<room> := <room>.lights`. So `l_1..l_6` are both the relay output and the "status" passed back next cycle and sent to Node-RED.
- Then the single set `l_1..l_6` 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
