27 KiB
PLC Algorithm Design - Lighting and Water Boiler Control
Overview
This document defines the PLC algorithms for the home automation system:
- Lighting Control - Improved logic with command-based control for Home Assistant and toggle-based for Zigbee switches
- 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
-
Backward Compatibility: The new lighting structure can coexist with existing code during migration.
-
Emergency Stop: Can be either a physical button (EtherCAT input) or a virtual command from Home Assistant/Node-RED.
-
Time Limit Configuration: Default is 8 hours but can be changed via
max_on_timeparameter or Node-RED configuration. -
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