Initial commit: CODESYS tree, integration docs, Node-RED scripts.

PLC application sources (NVL, rooms, boiler), HA/Node-RED integration guide, and NVL patch utilities.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-26 21:21:56 +03:00
commit d98c44bfa3
27 changed files with 3844 additions and 0 deletions

View File

@@ -0,0 +1,151 @@
(* === DECLARATION === *)
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
(* === IMPLEMENTATION === *)
// =====================================================
// 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;

View File

@@ -0,0 +1,57 @@
(* === DECLARATION === *)
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
(* === IMPLEMENTATION === *)
// =====================================================
// 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;

View File

@@ -0,0 +1,58 @@
(* === DECLARATION === *)
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
(* === IMPLEMENTATION === *)
// =====================================================
// 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;

View File

@@ -0,0 +1,20 @@
(* === DECLARATION === *)
FUNCTION_BLOCK fb_switch
VAR_INPUT
switches: struct_switches;
END_VAR
VAR_OUTPUT
lights: struct_lights;
END_VAR
VAR
all_off: fb_toogleButton;
all_on: fb_toogleButton;
sw_1: fb_toogleButton;
sw_2: fb_toogleButton;
sw_3: fb_toogleButton;
sw_4: fb_toogleButton;
sw_5: fb_toogleButton;
sw_6: fb_toogleButton;
END_VAR
(* === IMPLEMENTATION === *)

View File

@@ -0,0 +1,25 @@
(* === DECLARATION === *)
FUNCTION_BLOCK fb_toogleButton
VAR_INPUT
Input :BOOL;
END_VAR
VAR_OUTPUT
Output :BOOL;
END_VAR
VAR
Switch: BOOL;
Light: BOOL;
Flag1: BOOL;
Flag2: BOOL;
RT: R_TRIG;
Pulse: TP;
CTU_0: CTU;
CTD_0: CTD;
CTU_1: CTU;
CTU_2: CTU;
TOF_0: TOF;
END_VAR
(* === IMPLEMENTATION === *)