# PLC Algorithm Design - Lighting and Water Boiler Control ## Overviewp 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) ```iec 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) ```iec 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. ```iec 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 ```iec // ===================================================== // 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. ```iec 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 ```iec // ===================================================== // 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) ```iec 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) ```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_waterBoiler Simple ON/OFF boiler control with time limit and emergency stop. ```iec 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 ```iec // ===================================================== // 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 ```iec 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 ```iec // ===================================================== // 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) ```iec 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) ```iec 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