- Updated data structures for input and output to align with redesign recommendations - Clarified function block logic for fb_lightControl and fb_switch - Improved command handling and priority logic in the fb_lightControl implementation - Added detailed comments for better understanding of the algorithm flow This update ensures consistency with the redesign documentation and improves clarity for future development.
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
Alignment: This section follows the structures and logic from docs/redesign/fb_switch-redesign-recommendation.md: flat struct_switches / struct_lights, fb_lightControl with HA ON/OFF + Zigbee toggle only, and fb_switch 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_switches (flat, for Node-RED/JSON compatibility)
TYPE struct_switches :
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_lights (flat, control + status feedback)
TYPE struct_lights :
STRUCT
l_1: BOOL; // Light 1 control output
l_2: BOOL; // Light 2 control output
l_3: BOOL; // Light 3 control output
l_4: BOOL; // Light 4 control output
l_5: BOOL; // Light 5 control output
l_6: BOOL; // Light 6 control output
// Status feedback (read from actual relay outputs)
l_1_status: BOOL; // Actual relay 1 state
l_2_status: BOOL; // Actual relay 2 state
l_3_status: BOOL; // Actual relay 3 state
l_4_status: BOOL; // Actual relay 4 state
l_5_status: BOOL; // Actual relay 5 state
l_6_status: BOOL; // Actual relay 6 state
END_STRUCT
END_TYPE
1.3 Function Block: fb_lightControl (per Redesign)
Individual light control: HA ON/OFF + Zigbee toggle only. No all_on/all_off inside this FB.
FUNCTION_BLOCK fb_lightControl
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)
// =====================================================
// fb_lightControl - 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_switch (per Redesign)
Room-level block: 6× fb_lightControl; global commands applied by overwriting outputs.
FUNCTION_BLOCK fb_switch
VAR_INPUT
switches: struct_switches;
relay_status: struct_lights; // Read from EtherCAT outputs (actual relay states)
END_VAR
VAR_OUTPUT
lights: struct_lights;
END_VAR
VAR
l1: fb_lightControl;
l2: fb_lightControl;
l3: fb_lightControl;
l4: fb_lightControl;
l5: fb_lightControl;
l6: fb_lightControl;
END_VAR
Algorithm Logic
// =====================================================
// fb_switch - Implementation (per redesign)
// =====================================================
// Light 1 Control
l1(
ha_on := switches.ha_l1_on,
ha_off := switches.ha_l1_off,
zigbee_sw := switches.zigbee_sw1,
relay_status := relay_status.l_1_status
);
lights.l_1 := l1.light_output;
lights.l_1_status := l1.light_status;
// Light 2 Control
l2(
ha_on := switches.ha_l2_on,
ha_off := switches.ha_l2_off,
zigbee_sw := switches.zigbee_sw2,
relay_status := relay_status.l_2_status
);
lights.l_2 := l2.light_output;
lights.l_2_status := l2.light_status;
// Light 3 Control
l3(
ha_on := switches.ha_l3_on,
ha_off := switches.ha_l3_off,
zigbee_sw := switches.zigbee_sw3,
relay_status := relay_status.l_3_status
);
lights.l_3 := l3.light_output;
lights.l_3_status := l3.light_status;
// Light 4 Control
l4(
ha_on := switches.ha_l4_on,
ha_off := switches.ha_l4_off,
zigbee_sw := switches.zigbee_sw4,
relay_status := relay_status.l_4_status
);
lights.l_4 := l4.light_output;
lights.l_4_status := l4.light_status;
// Light 5 Control
l5(
ha_on := switches.ha_l5_on,
ha_off := switches.ha_l5_off,
zigbee_sw := switches.zigbee_sw5,
relay_status := relay_status.l_5_status
);
lights.l_5 := l5.light_output;
lights.l_5_status := l5.light_status;
// Light 6 Control
l6(
ha_on := switches.ha_l6_on,
ha_off := switches.ha_l6_off,
zigbee_sw := switches.zigbee_sw6,
relay_status := relay_status.l_6_status
);
lights.l_6 := l6.light_output;
lights.l_6_status := l6.light_status;
// Global Commands (overwrite outputs per redesign)
IF switches.ha_all_off THEN
lights.l_1 := FALSE;
lights.l_2 := FALSE;
lights.l_3 := FALSE;
lights.l_4 := FALSE;
lights.l_5 := FALSE;
lights.l_6 := FALSE;
ELSIF switches.ha_all_on THEN
lights.l_1 := TRUE;
lights.l_2 := TRUE;
lights.l_3 := TRUE;
lights.l_4 := TRUE;
lights.l_5 := TRUE;
lights.l_6 := TRUE;
END_IF;
Part 2: Water Boiler Control Algorithm
2.1 Design Goals
Based on your requirements:
- Simple ON/OFF control via relay output only
- Maximum heating time limit (safety feature)
- Emergency stop button support
- State monitoring and feedback
2.2 Data Structures
Boiler Commands (from Node-RED/Home Assistant)
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 (fb_switch per redesign)
masterBedroom: fb_switch;
masterBathroom: fb_switch;
bedroom_1: fb_switch;
bedroom_2: fb_switch;
bathroom: fb_switch;
guest_wc: fb_switch;
kitchen: fb_switch;
pantry: fb_switch;
livingRoom: fb_switch;
diningRoom: fb_switch;
entrance: fb_switch;
hallway: fb_switch;
veranda: fb_switch;
front: fb_switch;
back: fb_switch;
side: fb_switch;
// =====================================================
// 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 (fb_switch: switches + relay_status in, lights out)
masterBedroom(
switches := NVL_Receiver.masterBedroom,
relay_status := EtherCAT_RelayFeedback.masterBedroom // Actual relay states from I/O
);
NVL_Sender.l_masterBedroom := masterBedroom.lights;
EtherCAT_Outputs.masterBedroom_l1 := masterBedroom.lights.l_1;
EtherCAT_Outputs.masterBedroom_l2 := masterBedroom.lights.l_2;
// ... l_3..l_6 and 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 (struct_lights per room, per redesign)
l_masterBedroom: struct_lights;
l_masterBathroom: struct_lights;
l_bedroom_1: struct_lights;
l_bedroom_2: struct_lights;
l_bathroom: struct_lights;
l_guestWc: struct_lights;
l_kitchen: struct_lights;
l_pantry: struct_lights;
l_livingRoom: struct_lights;
l_dinningRoom: struct_lights;
l_entrance: struct_lights;
l_hallway: struct_lights;
l_outVeranda: struct_lights;
l_outFront: struct_lights;
l_outBack: struct_lights;
l_outSide: struct_lights;
// Boiler status (NEW)
boiler_status: struct_boiler_status;
END_VAR
5.2 NVL_Receiver (Node-RED → PLC)
VAR_GLOBAL
// Lighting commands (struct_switches per room, per redesign)
masterBedroom: struct_switches;
masterBathroom: struct_switches;
bedroom_1: struct_switches;
bedroom_2: struct_switches;
bathroom: struct_switches;
guestWc: struct_switches;
kitchen: struct_switches;
pantry: struct_switches;
livingRoom: struct_switches;
dinningRoom: struct_switches;
entrance: struct_switches;
hallway: struct_switches;
outVeranda: struct_switches;
outFront: struct_switches;
outBack: struct_switches;
outSide: struct_switches;
// 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
-
Redesign alignment: Lighting control (Part 1) follows
docs/redesign/fb_switch-redesign-recommendation.md: flatstruct_switches/struct_lights,fb_lightControl(HA ON/OFF + Zigbee toggle, no all_on/all_off),fb_switchwith global commands applied by overwriting outputs. Node-RED should sendha_l1_on/ha_l1_offandzigbee_sw1(edge) per the redesign. -
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.
-
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.1
Created: 2026-02-07
Updated: Aligned Part 1 (Lighting) with docs/redesign/fb_switch-redesign-recommendation.md.
Status: Design Complete - Ready for Implementation