Files
kkelomatic_home/docs/codesys/plc-algorithm-design.md
nearxos 9e56b35922 Refactor project structure and enhance documentation
- Improved organization of project files and directories
- Updated documentation index in docs/README.md for better navigation
- Enhanced clarity in naming conventions and design descriptions

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-07 22:00:01 +02:00

27 KiB

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)

System Architecture

┌─────────────────────────────────────────────────────────────────────────┐
│                          CODESYS PLC (Raspberry Pi)                     │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌─────────────────┐    ┌─────────────────┐    ┌─────────────────────┐ │
│  │   NVL_Receiver  │    │    MainTask     │    │    NVL_Sender       │ │
│  │   (from Node-RED)│    │    (4ms cycle)  │    │   (to Node-RED)     │ │
│  └────────┬────────┘    └────────┬────────┘    └──────────┬──────────┘ │
│           │                      │                        │             │
│           ▼                      ▼                        ▲             │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                          PLC_PRG                                │   │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────────┐  │   │
│  │  │   Lights    │  │   Boiler    │  │   Safety_Monitor        │  │   │
│  │  │  Program    │  │  Program    │  │      (future)           │  │   │
│  │  └──────┬──────┘  └──────┬──────┘  └─────────────────────────┘  │   │
│  └─────────┼────────────────┼──────────────────────────────────────┘   │
│            │                │                                          │
│            ▼                ▼                                          │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                     EtherCAT I/O                                │   │
│  │  ┌──────────────┐              ┌──────────────────────────────┐ │   │
│  │  │  EL1809      │              │   Output Module              │ │   │
│  │  │  16x DI 24V  │              │   16x DO (Relays)            │ │   │
│  │  │  (Switches)  │              │   (Lights + Boiler)          │ │   │
│  │  └──────────────┘              └──────────────────────────────┘ │   │
│  └─────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────┘
                    │                           │
                    ▼                           ▼
          ┌─────────────────┐         ┌─────────────────┐
          │  Physical       │         │  Physical       │
          │  Switches       │         │  Relays         │
          └─────────────────┘         └─────────────────┘

Part 1: Lighting Control Algorithm

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

Input Structure (from Node-RED/Home Assistant)

TYPE struct_light_commands :
STRUCT
    // Home Assistant Commands (momentary pulses)
    ha_on: BOOL;          // HA command: Turn ON (edge detected)
    ha_off: BOOL;         // HA command: Turn OFF (edge detected)
    
    // Zigbee Switch Input (toggle trigger)
    zigbee_toggle: BOOL;  // Zigbee button press (edge detected)
    
    // Global Commands
    all_on: BOOL;         // Global: All lights ON
    all_off: BOOL;        // Global: All lights OFF
END_STRUCT
END_TYPE

TYPE struct_room_commands :
STRUCT
    l_1: struct_light_commands;
    l_2: struct_light_commands;
    l_3: struct_light_commands;
    l_4: struct_light_commands;
    l_5: struct_light_commands;
    l_6: struct_light_commands;
    
    // Room-level global commands
    room_all_on: BOOL;
    room_all_off: BOOL;
END_STRUCT
END_TYPE

Output Structure (to Node-RED/Home Assistant)

TYPE struct_light_status :
STRUCT
    state: BOOL;          // Control output (to relay)
    relay_status: BOOL;   // Actual relay state (read back)
END_STRUCT
END_TYPE

TYPE struct_room_status :
STRUCT
    l_1: struct_light_status;
    l_2: struct_light_status;
    l_3: struct_light_status;
    l_4: struct_light_status;
    l_5: struct_light_status;
    l_6: struct_light_status;
END_STRUCT
END_TYPE

1.3 Function Block: fb_lightControl

Individual light control with command/toggle support.

FUNCTION_BLOCK fb_lightControl
VAR_INPUT
    // Commands
    ha_on: BOOL;              // Home Assistant ON command
    ha_off: BOOL;             // Home Assistant OFF command
    zigbee_toggle: BOOL;      // Zigbee toggle trigger
    all_on: BOOL;             // Global all-on command
    all_off: BOOL;            // Global all-off command
    
    // Status feedback
    relay_feedback: BOOL;     // Actual relay state (from EtherCAT)
END_VAR
VAR_OUTPUT
    output: BOOL;             // Control output to relay
    status: BOOL;             // Actual status (for feedback)
END_VAR
VAR
    // Edge detection
    r_trig_ha_on: R_TRIG;
    r_trig_ha_off: R_TRIG;
    r_trig_zigbee: R_TRIG;
    r_trig_all_on: R_TRIG;
    r_trig_all_off: R_TRIG;
    
    // Internal state
    light_state: BOOL := FALSE;
END_VAR

Algorithm Logic

// =====================================================
// fb_lightControl - Implementation
// =====================================================
// Priority (highest to lowest):
// 1. Global all_off  - Forces all lights OFF
// 2. Global all_on   - Forces all lights ON
// 3. HA OFF command  - Explicit OFF
// 4. HA ON command   - Explicit ON
// 5. Zigbee toggle   - Toggle current state
// =====================================================

// Edge detection for all commands
r_trig_ha_on(CLK := ha_on);
r_trig_ha_off(CLK := ha_off);
r_trig_zigbee(CLK := zigbee_toggle);
r_trig_all_on(CLK := all_on);
r_trig_all_off(CLK := all_off);

// State machine with priority handling
IF r_trig_all_off.Q THEN
    // Priority 1: Global OFF (highest priority)
    light_state := FALSE;
    
ELSIF r_trig_all_on.Q THEN
    // Priority 2: Global ON
    light_state := TRUE;
    
ELSIF r_trig_ha_off.Q THEN
    // Priority 3: HA OFF command
    light_state := FALSE;
    
ELSIF r_trig_ha_on.Q THEN
    // Priority 4: HA ON command
    light_state := TRUE;
    
ELSIF r_trig_zigbee.Q THEN
    // Priority 5: Zigbee toggle
    light_state := NOT light_state;
END_IF;

// Output assignment
output := light_state;

// Status feedback from actual relay
status := relay_feedback;

1.4 Function Block: fb_roomLights

Room-level lighting control with 6 lights per room.

FUNCTION_BLOCK fb_roomLights
VAR_INPUT
    commands: struct_room_commands;
    relay_feedback: ARRAY[1..6] OF BOOL;  // Actual relay states
END_VAR
VAR_OUTPUT
    outputs: ARRAY[1..6] OF BOOL;         // Control outputs
    status: struct_room_status;
END_VAR
VAR
    lights: ARRAY[1..6] OF fb_lightControl;
    i: INT;
END_VAR

Algorithm Logic

// =====================================================
// fb_roomLights - Implementation
// =====================================================

// Light 1
lights[1](
    ha_on := commands.l_1.ha_on,
    ha_off := commands.l_1.ha_off,
    zigbee_toggle := commands.l_1.zigbee_toggle,
    all_on := commands.room_all_on OR commands.l_1.all_on,
    all_off := commands.room_all_off OR commands.l_1.all_off,
    relay_feedback := relay_feedback[1]
);
outputs[1] := lights[1].output;
status.l_1.state := lights[1].output;
status.l_1.relay_status := lights[1].status;

// Light 2
lights[2](
    ha_on := commands.l_2.ha_on,
    ha_off := commands.l_2.ha_off,
    zigbee_toggle := commands.l_2.zigbee_toggle,
    all_on := commands.room_all_on OR commands.l_2.all_on,
    all_off := commands.room_all_off OR commands.l_2.all_off,
    relay_feedback := relay_feedback[2]
);
outputs[2] := lights[2].output;
status.l_2.state := lights[2].output;
status.l_2.relay_status := lights[2].status;

// Light 3
lights[3](
    ha_on := commands.l_3.ha_on,
    ha_off := commands.l_3.ha_off,
    zigbee_toggle := commands.l_3.zigbee_toggle,
    all_on := commands.room_all_on OR commands.l_3.all_on,
    all_off := commands.room_all_off OR commands.l_3.all_off,
    relay_feedback := relay_feedback[3]
);
outputs[3] := lights[3].output;
status.l_3.state := lights[3].output;
status.l_3.relay_status := lights[3].status;

// Light 4
lights[4](
    ha_on := commands.l_4.ha_on,
    ha_off := commands.l_4.ha_off,
    zigbee_toggle := commands.l_4.zigbee_toggle,
    all_on := commands.room_all_on OR commands.l_4.all_on,
    all_off := commands.room_all_off OR commands.l_4.all_off,
    relay_feedback := relay_feedback[4]
);
outputs[4] := lights[4].output;
status.l_4.state := lights[4].output;
status.l_4.relay_status := lights[4].status;

// Light 5
lights[5](
    ha_on := commands.l_5.ha_on,
    ha_off := commands.l_5.ha_off,
    zigbee_toggle := commands.l_5.zigbee_toggle,
    all_on := commands.room_all_on OR commands.l_5.all_on,
    all_off := commands.room_all_off OR commands.l_5.all_off,
    relay_feedback := relay_feedback[5]
);
outputs[5] := lights[5].output;
status.l_5.state := lights[5].output;
status.l_5.relay_status := lights[5].status;

// Light 6
lights[6](
    ha_on := commands.l_6.ha_on,
    ha_off := commands.l_6.ha_off,
    zigbee_toggle := commands.l_6.zigbee_toggle,
    all_on := commands.room_all_on OR commands.l_6.all_on,
    all_off := commands.room_all_off OR commands.l_6.all_off,
    relay_feedback := relay_feedback[6]
);
outputs[6] := lights[6].output;
status.l_6.state := lights[6].output;
status.l_6.relay_status := lights[6].status;

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)

TYPE struct_boiler_commands :
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)

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_waterBoiler

Simple ON/OFF boiler control with time limit and emergency stop.

FUNCTION_BLOCK fb_waterBoiler
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

// =====================================================
// fb_waterBoiler - 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_PRG Structure

PROGRAM PLC_PRG
VAR
    // =====================================================
    // LIGHTING CONTROL
    // =====================================================
    // Room instances
    masterBedroom: fb_roomLights;
    masterBathroom: fb_roomLights;
    bedroom_1: fb_roomLights;
    bedroom_2: fb_roomLights;
    bathroom: fb_roomLights;
    guest_wc: fb_roomLights;
    kitchen: fb_roomLights;
    pantry: fb_roomLights;
    livingRoom: fb_roomLights;
    diningRoom: fb_roomLights;
    entrance: fb_roomLights;
    hallway: fb_roomLights;
    veranda: fb_roomLights;
    front: fb_roomLights;
    back: fb_roomLights;
    side: fb_roomLights;
    
    // =====================================================
    // WATER BOILER CONTROL
    // =====================================================
    waterBoiler: fb_waterBoiler;
    
    // =====================================================
    // GLOBAL COMMANDS
    // =====================================================
    global_all_lights_off: BOOL;
    global_all_lights_on: BOOL;
END_VAR

3.2 Main Program Logic

// =====================================================
// PLC_PRG - Main Program Implementation
// =====================================================

// =====================================================
// SECTION 1: LIGHTING CONTROL
// =====================================================

// Master Bedroom
masterBedroom(
    commands := NVL_Receiver.masterBedroom,
    relay_feedback := EtherCAT_Outputs.masterBedroom_relay
);
NVL_Sender.l_masterBedroom := masterBedroom.status;
EtherCAT_Outputs.masterBedroom := masterBedroom.outputs;

// ... (repeat for all rooms)

// =====================================================
// SECTION 2: WATER BOILER CONTROL
// =====================================================

waterBoiler(
    ha_on := NVL_Receiver.boiler.ha_on,
    ha_off := NVL_Receiver.boiler.ha_off,
    schedule_on := NVL_Receiver.boiler.schedule_on,
    schedule_off := NVL_Receiver.boiler.schedule_off,
    emergency_stop := NVL_Receiver.boiler.emergency_stop 
                      OR DI_Emergency_Stop,  // Physical button OR remote
    max_on_time := T#8H
);

// Map outputs
EtherCAT_Outputs.boiler_relay := waterBoiler.relay_control;

// Status to Node-RED
NVL_Sender.boiler_status.state := waterBoiler.state;
NVL_Sender.boiler_status.relay_output := waterBoiler.relay_control;
NVL_Sender.boiler_status.on_time_minutes := DINT_TO_INT(waterBoiler.on_time_seconds / 60);
NVL_Sender.boiler_status.remaining_minutes := DINT_TO_INT(waterBoiler.remaining_seconds / 60);
NVL_Sender.boiler_status.emergency_active := waterBoiler.emergency_active;
NVL_Sender.boiler_status.time_limit_reached := waterBoiler.time_limit_reached;
NVL_Sender.boiler_status.error_state := waterBoiler.error_state;
NVL_Sender.boiler_status.error_code := waterBoiler.error_code;

Part 4: I/O Mapping

4.1 EtherCAT Digital Outputs

Address Function Description
16#1A00 - 16#1A0E Lighting Room light relays (existing)
16#1A0F Boiler Water boiler relay (new)

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)

Part 5: Network Variables

5.1 NVL_Sender (PLC → Node-RED)

VAR_GLOBAL
    // Lighting status (existing rooms)
    l_masterBedroom: struct_room_status;
    l_masterBathroom: struct_room_status;
    l_bedroom_1: struct_room_status;
    l_bedroom_2: struct_room_status;
    l_bathroom: struct_room_status;
    l_guestWc: struct_room_status;
    l_kitchen: struct_room_status;
    l_pantry: struct_room_status;
    l_livingRoom: struct_room_status;
    l_dinningRoom: struct_room_status;
    l_entrance: struct_room_status;
    l_hallway: struct_room_status;
    l_outVeranda: struct_room_status;
    l_outFront: struct_room_status;
    l_outBack: struct_room_status;
    l_outSide: struct_room_status;
    
    // Boiler status (NEW)
    boiler_status: struct_boiler_status;
END_VAR

5.2 NVL_Receiver (Node-RED → PLC)

VAR_GLOBAL
    // Lighting commands (existing rooms)
    masterBedroom: struct_room_commands;
    masterBathroom: struct_room_commands;
    bedroom_1: struct_room_commands;
    bedroom_2: struct_room_commands;
    bathroom: struct_room_commands;
    guestWc: struct_room_commands;
    kitchen: struct_room_commands;
    pantry: struct_room_commands;
    livingRoom: struct_room_commands;
    dinningRoom: struct_room_commands;
    entrance: struct_room_commands;
    hallway: struct_room_commands;
    outVeranda: struct_room_commands;
    outFront: struct_room_commands;
    outBack: struct_room_commands;
    outSide: struct_room_commands;
    
    // Boiler commands (NEW)
    boiler: struct_boiler_commands;
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. Backward Compatibility: The new lighting structure can coexist with existing code during migration.

  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.0
Created: 2026-02-07
Status: Design Complete - Ready for Implementation