diff --git a/codesys/src/GVL/GVL_IO.gvl b/codesys/src/GVL/GVL_IO.gvl
index b7c845e..85544ef 100644
--- a/codesys/src/GVL/GVL_IO.gvl
+++ b/codesys/src/GVL/GVL_IO.gvl
@@ -1,27 +1,27 @@
(*
GVL: GVL_IO
Option C relay feedback only. EL2809 DO: map channels directly in the device tree
- to NVL_Out (e.g. NVL_Out.l_masterBedroom.l_1..l_6, ...) and boiler relay.
+ to NVL_Out (e.g. NVL_Out.l_open_plan_living_room.l_1..l_6, ...) and boiler relay.
Initialize EtherCAT_RelayFeedback to zero at startup (or use default init).
*)
VAR_GLOBAL
// ---------- Option C: relay feedback (copy of output for next cycle) ----------
EtherCAT_RelayFeedback: STRUCT
- masterBedroom: struct_room_outs;
- masterBathroom: struct_room_outs;
- bedroom_1: struct_room_outs;
- bedroom_2: struct_room_outs;
- bathroom: struct_room_outs;
- guest_wc: struct_room_outs;
- kitchen: struct_room_outs;
- pantry: struct_room_outs;
- livingRoom: struct_room_outs;
- diningRoom: struct_room_outs;
- entrance: struct_room_outs;
- hallway: struct_room_outs;
- veranda: struct_room_outs;
- front: struct_room_outs;
- back: struct_room_outs;
- side: struct_room_outs;
+ open_plan_living_room: struct_room_outs;
+ open_plan_dining_room: struct_room_outs;
+ open_plan_entrance: struct_room_outs;
+ open_plan_guest_wc: struct_room_outs;
+ kitchen_kitchen: struct_room_outs;
+ kitchen_pantry: struct_room_outs;
+ bedrooms_office: struct_room_outs;
+ bedrooms_hallway: struct_room_outs;
+ bedrooms_laundry: struct_room_outs;
+ bedrooms_shower: struct_room_outs;
+ bedrooms_bedroom: struct_room_outs;
+ master_bedroom_suite: struct_room_outs;
+ master_bedroom_bathroom: struct_room_outs;
+ exterior_veranda: struct_room_outs;
+ exterior_entrance: struct_room_outs;
+ exterior_yard: struct_room_outs;
END_STRUCT;
END_VAR
diff --git a/codesys/src/GVL/GVL_NVL_placeholders.gvl b/codesys/src/GVL/GVL_NVL_placeholders.gvl
index 5e39045..0d9cd14 100644
--- a/codesys/src/GVL/GVL_NVL_placeholders.gvl
+++ b/codesys/src/GVL/GVL_NVL_placeholders.gvl
@@ -5,42 +5,42 @@
*)
VAR_GLOBAL
NVL_In: STRUCT
- masterBedroom: struct_room_cmds;
- masterBathroom: struct_room_cmds;
- bedroom_1: struct_room_cmds;
- bedroom_2: struct_room_cmds;
- bathroom: struct_room_cmds;
- guestWc: struct_room_cmds;
- kitchen: struct_room_cmds;
- pantry: struct_room_cmds;
- livingRoom: struct_room_cmds;
- dinningRoom: struct_room_cmds;
- entrance: struct_room_cmds;
- hallway: struct_room_cmds;
- outVeranda: struct_room_cmds;
- outFront: struct_room_cmds;
- outBack: struct_room_cmds;
- outSide: struct_room_cmds;
+ open_plan_living_room: struct_room_cmds;
+ open_plan_dining_room: struct_room_cmds;
+ open_plan_entrance: struct_room_cmds;
+ open_plan_guest_wc: struct_room_cmds;
+ kitchen_kitchen: struct_room_cmds;
+ kitchen_pantry: struct_room_cmds;
+ bedrooms_office: struct_room_cmds;
+ bedrooms_hallway: struct_room_cmds;
+ bedrooms_laundry: struct_room_cmds;
+ bedrooms_shower: struct_room_cmds;
+ bedrooms_bedroom: struct_room_cmds;
+ master_bedroom_suite: struct_room_cmds;
+ master_bedroom_bathroom: struct_room_cmds;
+ exterior_veranda: struct_room_cmds;
+ exterior_entrance: struct_room_cmds;
+ exterior_yard: struct_room_cmds;
boiler: struct_boiler_cmd;
END_STRUCT;
NVL_Out: STRUCT
- l_masterBedroom: struct_room_outs;
- l_masterBathroom: struct_room_outs;
- l_bedroom_1: struct_room_outs;
- l_bedroom_2: struct_room_outs;
- l_bathroom: struct_room_outs;
- l_guestWc: struct_room_outs;
- l_kitchen: struct_room_outs;
- l_pantry: struct_room_outs;
- l_livingRoom: struct_room_outs;
- l_dinningRoom: struct_room_outs;
- l_entrance: struct_room_outs;
- l_hallway: struct_room_outs;
- l_outVeranda: struct_room_outs;
- l_outFront: struct_room_outs;
- l_outBack: struct_room_outs;
- l_outSide: struct_room_outs;
+ l_open_plan_living_room: struct_room_outs;
+ l_open_plan_dining_room: struct_room_outs;
+ l_open_plan_entrance: struct_room_outs;
+ l_open_plan_guest_wc: struct_room_outs;
+ l_kitchen_kitchen: struct_room_outs;
+ l_kitchen_pantry: struct_room_outs;
+ l_bedrooms_office: struct_room_outs;
+ l_bedrooms_hallway: struct_room_outs;
+ l_bedrooms_laundry: struct_room_outs;
+ l_bedrooms_shower: struct_room_outs;
+ l_bedrooms_bedroom: struct_room_outs;
+ l_master_bedroom_suite: struct_room_outs;
+ l_master_bedroom_bathroom: struct_room_outs;
+ l_exterior_veranda: struct_room_outs;
+ l_exterior_entrance: struct_room_outs;
+ l_exterior_yard: struct_room_outs;
boiler_status: struct_boiler_status;
END_STRUCT;
diff --git a/codesys/src/NVL/README.md b/codesys/src/NVL/README.md
index 5f5d391..1d7ffca 100644
--- a/codesys/src/NVL/README.md
+++ b/codesys/src/NVL/README.md
@@ -23,12 +23,14 @@ This folder describes the **network variable** setup used for CODESYS ↔ Node-R
| Variable | Type | Description |
|----------|------|-------------|
-| `l_masterBedroom` | struct_room_outs | 12 BOOLs (l_1..l_6, l_1_status..l_6_status) |
-| `l_masterBathroom` | struct_room_outs | same |
-| `l_bedroom_1` .. `l_bedroom_2` | struct_room_outs | same |
-| `l_bathroom`, `l_guestWc`, `l_kitchen`, `l_pantry` | struct_room_outs | same |
-| `l_livingRoom`, `l_dinningRoom`, `l_entrance`, `l_hallway` | struct_room_outs | same |
-| `l_outVeranda`, `l_outFront`, `l_outBack`, `l_outSide` | struct_room_outs | same |
+| `l_open_plan_living_room` | struct_room_outs | 6 BOOLs (`l_1..l_6`) |
+| `l_open_plan_dining_room` | struct_room_outs | same |
+| `l_open_plan_entrance` | struct_room_outs | same |
+| `l_open_plan_guest_wc` | struct_room_outs | same |
+| `l_kitchen_kitchen`, `l_kitchen_pantry` | struct_room_outs | same |
+| `l_bedrooms_office`, `l_bedrooms_hallway`, `l_bedrooms_laundry`, `l_bedrooms_shower`, `l_bedrooms_bedroom` | struct_room_outs | same |
+| `l_master_bedroom_suite`, `l_master_bedroom_bathroom` | struct_room_outs | same |
+| `l_exterior_veranda`, `l_exterior_entrance`, `l_exterior_yard` | struct_room_outs | same |
| `boiler_status` | struct_boiler_status | state, relay_output, on_time_minutes, remaining_minutes, emergency_active, time_limit_reached, error_state, error_code |
**In CODESYS:** Create an **NVL Sender** (Network Variable List), bind it to the **NVL_Out** structure (the same struct as in `GVL_NVL_placeholders.gvl` → `NVL_Out`). Set protocol to UDP, destination IP/port to your Node-RED host, task and interval (e.g. EtherCAT_Task, 50 ms).
@@ -43,7 +45,7 @@ This folder describes the **network variable** setup used for CODESYS ↔ Node-R
| Variable | Type | Description |
|----------|------|-------------|
-| `masterBedroom` .. `side` (15 rooms) | struct_room_cmds | ha_l1_on, ha_l1_off, ... ha_l6_on, ha_l6_off, zigbee_sw1..6, ha_all_on, ha_all_off |
+| `open_plan_living_room` .. `exterior_yard` (16 rooms) | struct_room_cmds | ha_l1_on, ha_l1_off, ... ha_l6_on, ha_l6_off, zigbee_sw1..6, ha_all_on, ha_all_off |
| `boiler` | struct_boiler_cmd | ha_on, ha_off, schedule_on, schedule_off, emergency_stop, max_on_time_minutes |
**In CODESYS:** Create an **NVL Receiver**, bind it to the **NVL_In** structure. Set protocol to UDP, listen port, task and interval. The PLC will overwrite `NVL_In` with received data each cycle.
diff --git a/codesys/src/NVL/nodered-payload.md b/codesys/src/NVL/nodered-payload.md
index a2cea44..73b4843 100644
--- a/codesys/src/NVL/nodered-payload.md
+++ b/codesys/src/NVL/nodered-payload.md
@@ -25,7 +25,7 @@ PLC sends one block: all rooms’ `struct_room_outs` followed by `struct_boiler_
6 BOOLs in order: `l_1, l_2, l_3, l_4, l_5, l_6` (light state; with Option C this is both output and status).
- **Size per room:** 6 bytes.
-- **Rooms in order:** l_masterBedroom, l_masterBathroom, l_bedroom_1, l_bedroom_2, l_bathroom, l_guestWc, l_kitchen, l_pantry, l_livingRoom, l_dinningRoom, l_entrance, l_hallway, l_outVeranda, l_outFront, l_outBack, l_outSide.
+- **Rooms in order:** l_open_plan_living_room, l_open_plan_dining_room, l_open_plan_entrance, l_open_plan_guest_wc, l_kitchen_kitchen, l_kitchen_pantry, l_bedrooms_office, l_bedrooms_hallway, l_bedrooms_laundry, l_bedrooms_shower, l_bedrooms_bedroom, l_master_bedroom_suite, l_master_bedroom_bathroom, l_exterior_veranda, l_exterior_entrance, l_exterior_yard.
- **Total for lights:** 16 × 6 = **96 bytes**.
### struct_boiler_status (after all rooms)
@@ -50,7 +50,7 @@ PLC sends one block: all rooms’ `struct_room_outs` followed by `struct_boiler_
// msg.payload = Buffer (UDP payload from PLC)
const buf = msg.payload;
const roomSize = 6;
-const roomNames = ['l_masterBedroom','l_masterBathroom','l_bedroom_1','l_bedroom_2','l_bathroom','l_guestWc','l_kitchen','l_pantry','l_livingRoom','l_dinningRoom','l_entrance','l_hallway','l_outVeranda','l_outFront','l_outBack','l_outSide'];
+const roomNames = ['l_open_plan_living_room','l_open_plan_dining_room','l_open_plan_entrance','l_open_plan_guest_wc','l_kitchen_kitchen','l_kitchen_pantry','l_bedrooms_office','l_bedrooms_hallway','l_bedrooms_laundry','l_bedrooms_shower','l_bedrooms_bedroom','l_master_bedroom_suite','l_master_bedroom_bathroom','l_exterior_veranda','l_exterior_entrance','l_exterior_yard'];
const out = { rooms: {}, boiler_status: {} };
@@ -90,7 +90,7 @@ PLC expects one block: all rooms’ `struct_room_cmds` followed by `struct_boile
ha_l1_on, ha_l1_off, ha_l2_on, ha_l2_off, ha_l3_on, ha_l3_off, ha_l4_on, ha_l4_off, ha_l5_on, ha_l5_off, ha_l6_on, ha_l6_off, zigbee_sw1..6, ha_all_on, ha_all_off.
- **Size per room:** 20 bytes.
-- **Rooms in order:** masterBedroom, masterBathroom, bedroom_1, bedroom_2, bathroom, guestWc, kitchen, pantry, livingRoom, dinningRoom, entrance, hallway, outVeranda, outFront, outBack, outSide.
+- **Rooms in order:** open_plan_living_room, open_plan_dining_room, open_plan_entrance, open_plan_guest_wc, kitchen_kitchen, kitchen_pantry, bedrooms_office, bedrooms_hallway, bedrooms_laundry, bedrooms_shower, bedrooms_bedroom, master_bedroom_suite, master_bedroom_bathroom, exterior_veranda, exterior_entrance, exterior_yard.
- **Total for rooms:** 16 × 20 = **320 bytes**.
### struct_boiler_cmd
@@ -112,7 +112,7 @@ ha_l1_on, ha_l1_off, ha_l2_on, ha_l2_off, ha_l3_on, ha_l3_off, ha_l4_on, ha_l4_o
```javascript
// Build binary payload for PLC from commands
const roomSize = 20;
-const roomNames = ['masterBedroom','masterBathroom','bedroom_1','bedroom_2','bathroom','guestWc','kitchen','pantry','livingRoom','dinningRoom','entrance','hallway','outVeranda','outFront','outBack','outSide'];
+const roomNames = ['open_plan_living_room','open_plan_dining_room','open_plan_entrance','open_plan_guest_wc','kitchen_kitchen','kitchen_pantry','bedrooms_office','bedrooms_hallway','bedrooms_laundry','bedrooms_shower','bedrooms_bedroom','master_bedroom_suite','master_bedroom_bathroom','exterior_veranda','exterior_entrance','exterior_yard'];
const buf = Buffer.alloc(328);
const rooms = msg.payload.rooms || {};
diff --git a/codesys/src/POUs/PLC_App.st b/codesys/src/POUs/PLC_App.st
index fe0d1b1..9554e43 100644
--- a/codesys/src/POUs/PLC_App.st
+++ b/codesys/src/POUs/PLC_App.st
@@ -2,112 +2,112 @@
POU: PLC_App
Main program (Option C: copy of output as relay feedback).
Requires: NVL_In, NVL_Out, GVL_IO (EtherCAT_RelayFeedback).
- Map EL2809 DO channels in the device tree directly to NVL_Out (e.g. Ch0..5 = l_masterBedroom.l_1..l_6)
+ Map EL2809 DO channels in the device tree directly to NVL_Out (e.g. Ch0..5 = l_open_plan_living_room.l_1..l_6)
and Ch15 to boiler relay (e.g. NVL_Out.boiler_status.relay_output or a GVL BOOL).
Optional: DI_Emergency_Stop (physical E-Stop input).
*)
PROGRAM PLC_App
VAR
- masterBedroom: fb_room;
- masterBathroom: fb_room;
- bedroom_1: fb_room;
- bedroom_2: fb_room;
- bathroom: fb_room;
- guest_wc: fb_room;
- kitchen: fb_room;
- pantry: fb_room;
- livingRoom: fb_room;
- diningRoom: fb_room;
- entrance: fb_room;
- hallway: fb_room;
- veranda: fb_room;
- front: fb_room;
- back: fb_room;
- side: fb_room;
+ open_plan_living_room: fb_room;
+ open_plan_dining_room: fb_room;
+ open_plan_entrance: fb_room;
+ open_plan_guest_wc: fb_room;
+ kitchen_kitchen: fb_room;
+ kitchen_pantry: fb_room;
+ bedrooms_office: fb_room;
+ bedrooms_hallway: fb_room;
+ bedrooms_laundry: fb_room;
+ bedrooms_shower: fb_room;
+ bedrooms_bedroom: fb_room;
+ master_bedroom_suite: fb_room;
+ master_bedroom_bathroom: fb_room;
+ exterior_veranda: fb_room;
+ exterior_entrance: fb_room;
+ exterior_yard: fb_room;
boiler: fb_boiler;
END_VAR
// ========== SECTION 1: LIGHTING (Option C: relay feedback = copy of output) ==========
-// Master Bedroom
-masterBedroom(switches := NVL_In.masterBedroom, relay_status := EtherCAT_RelayFeedback.masterBedroom);
-EtherCAT_RelayFeedback.masterBedroom := masterBedroom.lights;
-NVL_Out.l_masterBedroom := masterBedroom.lights;
+// Open Plan - Living Room
+open_plan_living_room(switches := NVL_In.open_plan_living_room, relay_status := EtherCAT_RelayFeedback.open_plan_living_room);
+EtherCAT_RelayFeedback.open_plan_living_room := open_plan_living_room.lights;
+NVL_Out.l_open_plan_living_room := open_plan_living_room.lights;
-// Master Bathroom
-masterBathroom(switches := NVL_In.masterBathroom, relay_status := EtherCAT_RelayFeedback.masterBathroom);
-EtherCAT_RelayFeedback.masterBathroom := masterBathroom.lights;
-NVL_Out.l_masterBathroom := masterBathroom.lights;
+// Open Plan - Dining Room
+open_plan_dining_room(switches := NVL_In.open_plan_dining_room, relay_status := EtherCAT_RelayFeedback.open_plan_dining_room);
+EtherCAT_RelayFeedback.open_plan_dining_room := open_plan_dining_room.lights;
+NVL_Out.l_open_plan_dining_room := open_plan_dining_room.lights;
-// Bedroom 1
-bedroom_1(switches := NVL_In.bedroom_1, relay_status := EtherCAT_RelayFeedback.bedroom_1);
-EtherCAT_RelayFeedback.bedroom_1 := bedroom_1.lights;
-NVL_Out.l_bedroom_1 := bedroom_1.lights;
+// Open Plan - Entrance
+open_plan_entrance(switches := NVL_In.open_plan_entrance, relay_status := EtherCAT_RelayFeedback.open_plan_entrance);
+EtherCAT_RelayFeedback.open_plan_entrance := open_plan_entrance.lights;
+NVL_Out.l_open_plan_entrance := open_plan_entrance.lights;
-// Bedroom 2
-bedroom_2(switches := NVL_In.bedroom_2, relay_status := EtherCAT_RelayFeedback.bedroom_2);
-EtherCAT_RelayFeedback.bedroom_2 := bedroom_2.lights;
-NVL_Out.l_bedroom_2 := bedroom_2.lights;
-
-// Bathroom
-bathroom(switches := NVL_In.bathroom, relay_status := EtherCAT_RelayFeedback.bathroom);
-EtherCAT_RelayFeedback.bathroom := bathroom.lights;
-NVL_Out.l_bathroom := bathroom.lights;
-
-// Guest WC
-guest_wc(switches := NVL_In.guestWc, relay_status := EtherCAT_RelayFeedback.guest_wc);
-EtherCAT_RelayFeedback.guest_wc := guest_wc.lights;
-NVL_Out.l_guestWc := guest_wc.lights;
+// Open Plan - Guest WC
+open_plan_guest_wc(switches := NVL_In.open_plan_guest_wc, relay_status := EtherCAT_RelayFeedback.open_plan_guest_wc);
+EtherCAT_RelayFeedback.open_plan_guest_wc := open_plan_guest_wc.lights;
+NVL_Out.l_open_plan_guest_wc := open_plan_guest_wc.lights;
// Kitchen
-kitchen(switches := NVL_In.kitchen, relay_status := EtherCAT_RelayFeedback.kitchen);
-EtherCAT_RelayFeedback.kitchen := kitchen.lights;
-NVL_Out.l_kitchen := kitchen.lights;
+kitchen_kitchen(switches := NVL_In.kitchen_kitchen, relay_status := EtherCAT_RelayFeedback.kitchen_kitchen);
+EtherCAT_RelayFeedback.kitchen_kitchen := kitchen_kitchen.lights;
+NVL_Out.l_kitchen_kitchen := kitchen_kitchen.lights;
// Pantry
-pantry(switches := NVL_In.pantry, relay_status := EtherCAT_RelayFeedback.pantry);
-EtherCAT_RelayFeedback.pantry := pantry.lights;
-NVL_Out.l_pantry := pantry.lights;
+kitchen_pantry(switches := NVL_In.kitchen_pantry, relay_status := EtherCAT_RelayFeedback.kitchen_pantry);
+EtherCAT_RelayFeedback.kitchen_pantry := kitchen_pantry.lights;
+NVL_Out.l_kitchen_pantry := kitchen_pantry.lights;
-// Living Room
-livingRoom(switches := NVL_In.livingRoom, relay_status := EtherCAT_RelayFeedback.livingRoom);
-EtherCAT_RelayFeedback.livingRoom := livingRoom.lights;
-NVL_Out.l_livingRoom := livingRoom.lights;
+// Bedrooms - Office
+bedrooms_office(switches := NVL_In.bedrooms_office, relay_status := EtherCAT_RelayFeedback.bedrooms_office);
+EtherCAT_RelayFeedback.bedrooms_office := bedrooms_office.lights;
+NVL_Out.l_bedrooms_office := bedrooms_office.lights;
-// Dining Room
-diningRoom(switches := NVL_In.dinningRoom, relay_status := EtherCAT_RelayFeedback.diningRoom);
-EtherCAT_RelayFeedback.diningRoom := diningRoom.lights;
-NVL_Out.l_dinningRoom := diningRoom.lights;
+// Bedrooms - Hallway
+bedrooms_hallway(switches := NVL_In.bedrooms_hallway, relay_status := EtherCAT_RelayFeedback.bedrooms_hallway);
+EtherCAT_RelayFeedback.bedrooms_hallway := bedrooms_hallway.lights;
+NVL_Out.l_bedrooms_hallway := bedrooms_hallway.lights;
-// Entrance
-entrance(switches := NVL_In.entrance, relay_status := EtherCAT_RelayFeedback.entrance);
-EtherCAT_RelayFeedback.entrance := entrance.lights;
-NVL_Out.l_entrance := entrance.lights;
+// Bedrooms - Laundry
+bedrooms_laundry(switches := NVL_In.bedrooms_laundry, relay_status := EtherCAT_RelayFeedback.bedrooms_laundry);
+EtherCAT_RelayFeedback.bedrooms_laundry := bedrooms_laundry.lights;
+NVL_Out.l_bedrooms_laundry := bedrooms_laundry.lights;
-// Hallway
-hallway(switches := NVL_In.hallway, relay_status := EtherCAT_RelayFeedback.hallway);
-EtherCAT_RelayFeedback.hallway := hallway.lights;
-NVL_Out.l_hallway := hallway.lights;
+// Bedrooms - Shower
+bedrooms_shower(switches := NVL_In.bedrooms_shower, relay_status := EtherCAT_RelayFeedback.bedrooms_shower);
+EtherCAT_RelayFeedback.bedrooms_shower := bedrooms_shower.lights;
+NVL_Out.l_bedrooms_shower := bedrooms_shower.lights;
-// Veranda
-veranda(switches := NVL_In.outVeranda, relay_status := EtherCAT_RelayFeedback.veranda);
-EtherCAT_RelayFeedback.veranda := veranda.lights;
-NVL_Out.l_outVeranda := veranda.lights;
+// Bedrooms - Bedroom
+bedrooms_bedroom(switches := NVL_In.bedrooms_bedroom, relay_status := EtherCAT_RelayFeedback.bedrooms_bedroom);
+EtherCAT_RelayFeedback.bedrooms_bedroom := bedrooms_bedroom.lights;
+NVL_Out.l_bedrooms_bedroom := bedrooms_bedroom.lights;
-// Front
-front(switches := NVL_In.outFront, relay_status := EtherCAT_RelayFeedback.front);
-EtherCAT_RelayFeedback.front := front.lights;
-NVL_Out.l_outFront := front.lights;
+// Master Bedroom - Suite
+master_bedroom_suite(switches := NVL_In.master_bedroom_suite, relay_status := EtherCAT_RelayFeedback.master_bedroom_suite);
+EtherCAT_RelayFeedback.master_bedroom_suite := master_bedroom_suite.lights;
+NVL_Out.l_master_bedroom_suite := master_bedroom_suite.lights;
-// Back
-back(switches := NVL_In.outBack, relay_status := EtherCAT_RelayFeedback.back);
-EtherCAT_RelayFeedback.back := back.lights;
-NVL_Out.l_outBack := back.lights;
+// Master Bedroom - Bathroom
+master_bedroom_bathroom(switches := NVL_In.master_bedroom_bathroom, relay_status := EtherCAT_RelayFeedback.master_bedroom_bathroom);
+EtherCAT_RelayFeedback.master_bedroom_bathroom := master_bedroom_bathroom.lights;
+NVL_Out.l_master_bedroom_bathroom := master_bedroom_bathroom.lights;
-// Side
-side(switches := NVL_In.outSide, relay_status := EtherCAT_RelayFeedback.side);
-EtherCAT_RelayFeedback.side := side.lights;
-NVL_Out.l_outSide := side.lights;
+// Exterior - Veranda
+exterior_veranda(switches := NVL_In.exterior_veranda, relay_status := EtherCAT_RelayFeedback.exterior_veranda);
+EtherCAT_RelayFeedback.exterior_veranda := exterior_veranda.lights;
+NVL_Out.l_exterior_veranda := exterior_veranda.lights;
+
+// Exterior - Entrance
+exterior_entrance(switches := NVL_In.exterior_entrance, relay_status := EtherCAT_RelayFeedback.exterior_entrance);
+EtherCAT_RelayFeedback.exterior_entrance := exterior_entrance.lights;
+NVL_Out.l_exterior_entrance := exterior_entrance.lights;
+
+// Exterior - Yard
+exterior_yard(switches := NVL_In.exterior_yard, relay_status := EtherCAT_RelayFeedback.exterior_yard);
+EtherCAT_RelayFeedback.exterior_yard := exterior_yard.lights;
+NVL_Out.l_exterior_yard := exterior_yard.lights;
// ========== SECTION 2: BOILER ==========
diff --git a/docs/Electrical/Ligts_Zones.csv b/docs/Electrical/Ligts_Zones.csv
new file mode 100644
index 0000000..35bba97
--- /dev/null
+++ b/docs/Electrical/Ligts_Zones.csv
@@ -0,0 +1,42 @@
+#,Zone,Area,Light
+1,L1,Open Plan,Living Room Main
+2,L1,Open Plan,Living Room Spots
+3,L1,Open Plan,Dining room Main
+4,L1,Open Plan,Dining room Spots
+5,L1,Open Plan,Entrance Mirror
+6,L1,Open Plan,Entrance Main
+7,L1,Open Plan,Guest WC Main
+8,L1,Open Plan,Guiest WC Strip
+9,L1,Open Plan,Guest WC Fan
+10,L1,Open Plan,Living Room Strip
+11,L1,Open Plan,Living Room Strip
+12,L2,Ktchen,Kitchen Main
+13,L2,Ktchen,Island
+14,L2,Ktchen,Kitchen Cabinets
+15,L2,Ktchen,Fridge
+16,L2,Ktchen,Pantry
+17,L3,Bedrooms,Office Main
+18,L3,Bedrooms,Office Strip
+19,L3,Bedrooms,Hallway
+20,L3,Bedrooms,Wachine Machine
+21,L3,Bedrooms,Shower spots
+22,L3,Bedrooms,Shower fan
+23,L3,Bedrooms,Bedroom Main
+24,L3,Bedrooms,Bedroom Spots
+25,L4,Master Bedroom,Main
+26,L4,Master Bedroom,Spots
+27,L4,Master Bedroom,Strip
+28,L4,Master Bedroom,Door
+29,L4,Master Bedroom,Dresser
+30,L4,Master Bedroom,bathroom spots
+31,L4,Master Bedroom,bathroom fan
+32,L5,Exterior,Veranda
+33,L5,Exterior,BBQ
+34,L5,Exterior,Entrance Door
+35,L5,Exterior,Front Lights
+36,L5,Exterior,backyard flood light 1
+37,L5,Exterior,backyard flood light 2
+38,L5,Exterior,Master Bedroom flood light
+39,L5,exterior,Driveway
+40,L5,,
+41,L5,,
diff --git a/docs/Electrical/lights_zones_canonical.csv b/docs/Electrical/lights_zones_canonical.csv
new file mode 100644
index 0000000..d36339f
--- /dev/null
+++ b/docs/Electrical/lights_zones_canonical.csv
@@ -0,0 +1,40 @@
+circuit_no,zone,area,room,light_type,index,light_no,codesys_in_key,codesys_out_key,nodered_room_key,ha_entity_id,source_light_label
+1,l1,open_plan,living_room,main,1,1,open_plan_living_room,l_open_plan_living_room,open_plan_living_room,input_boolean.open_plan_living_room_main_1,Living Room Main
+2,l1,open_plan,living_room,spots,1,2,open_plan_living_room,l_open_plan_living_room,open_plan_living_room,input_boolean.open_plan_living_room_spots_1,Living Room Spots
+3,l1,open_plan,dining_room,main,1,1,open_plan_dining_room,l_open_plan_dining_room,open_plan_dining_room,input_boolean.open_plan_dining_room_main_1,Dining room Main
+4,l1,open_plan,dining_room,spots,1,2,open_plan_dining_room,l_open_plan_dining_room,open_plan_dining_room,input_boolean.open_plan_dining_room_spots_1,Dining room Spots
+5,l1,open_plan,entrance,mirror,1,1,open_plan_entrance,l_open_plan_entrance,open_plan_entrance,input_boolean.open_plan_entrance_mirror_1,Entrance Mirror
+6,l1,open_plan,entrance,main,1,2,open_plan_entrance,l_open_plan_entrance,open_plan_entrance,input_boolean.open_plan_entrance_main_1,Entrance Main
+7,l1,open_plan,guest_wc,main,1,1,open_plan_guest_wc,l_open_plan_guest_wc,open_plan_guest_wc,input_boolean.open_plan_guest_wc_main_1,Guest WC Main
+8,l1,open_plan,guest_wc,strip,1,2,open_plan_guest_wc,l_open_plan_guest_wc,open_plan_guest_wc,input_boolean.open_plan_guest_wc_strip_1,Guiest WC Strip
+9,l1,open_plan,guest_wc,fan,1,3,open_plan_guest_wc,l_open_plan_guest_wc,open_plan_guest_wc,input_boolean.open_plan_guest_wc_fan_1,Guest WC Fan
+10,l1,open_plan,living_room,strip,1,3,open_plan_living_room,l_open_plan_living_room,open_plan_living_room,input_boolean.open_plan_living_room_strip_1,Living Room Strip
+11,l1,open_plan,living_room,strip,2,4,open_plan_living_room,l_open_plan_living_room,open_plan_living_room,input_boolean.open_plan_living_room_strip_2,Living Room Strip
+12,l2,kitchen,kitchen,main,1,1,kitchen_kitchen,l_kitchen_kitchen,kitchen_kitchen,input_boolean.kitchen_kitchen_main_1,Kitchen Main
+13,l2,kitchen,kitchen,island,1,2,kitchen_kitchen,l_kitchen_kitchen,kitchen_kitchen,input_boolean.kitchen_kitchen_island_1,Island
+14,l2,kitchen,kitchen,cabinets,1,3,kitchen_kitchen,l_kitchen_kitchen,kitchen_kitchen,input_boolean.kitchen_kitchen_cabinets_1,Kitchen Cabinets
+15,l2,kitchen,kitchen,fridge,1,4,kitchen_kitchen,l_kitchen_kitchen,kitchen_kitchen,input_boolean.kitchen_kitchen_fridge_1,Fridge
+16,l2,kitchen,pantry,main,1,1,kitchen_pantry,l_kitchen_pantry,kitchen_pantry,input_boolean.kitchen_pantry_main_1,Pantry
+17,l3,bedrooms,office,main,1,1,bedrooms_office,l_bedrooms_office,bedrooms_office,input_boolean.bedrooms_office_main_1,Office Main
+18,l3,bedrooms,office,strip,1,2,bedrooms_office,l_bedrooms_office,bedrooms_office,input_boolean.bedrooms_office_strip_1,Office Strip
+19,l3,bedrooms,hallway,main,1,1,bedrooms_hallway,l_bedrooms_hallway,bedrooms_hallway,input_boolean.bedrooms_hallway_main_1,Hallway
+20,l3,bedrooms,laundry,main,1,1,bedrooms_laundry,l_bedrooms_laundry,bedrooms_laundry,input_boolean.bedrooms_laundry_main_1,Wachine Machine
+21,l3,bedrooms,shower,spots,1,1,bedrooms_shower,l_bedrooms_shower,bedrooms_shower,input_boolean.bedrooms_shower_spots_1,Shower spots
+22,l3,bedrooms,shower,fan,1,2,bedrooms_shower,l_bedrooms_shower,bedrooms_shower,input_boolean.bedrooms_shower_fan_1,Shower fan
+23,l3,bedrooms,bedroom,main,1,1,bedrooms_bedroom,l_bedrooms_bedroom,bedrooms_bedroom,input_boolean.bedrooms_bedroom_main_1,Bedroom Main
+24,l3,bedrooms,bedroom,spots,1,2,bedrooms_bedroom,l_bedrooms_bedroom,bedrooms_bedroom,input_boolean.bedrooms_bedroom_spots_1,Bedroom Spots
+25,l4,master_bedroom,suite,main,1,1,master_bedroom_suite,l_master_bedroom_suite,master_bedroom_suite,input_boolean.master_bedroom_suite_main_1,Main
+26,l4,master_bedroom,suite,spots,1,2,master_bedroom_suite,l_master_bedroom_suite,master_bedroom_suite,input_boolean.master_bedroom_suite_spots_1,Spots
+27,l4,master_bedroom,suite,strip,1,3,master_bedroom_suite,l_master_bedroom_suite,master_bedroom_suite,input_boolean.master_bedroom_suite_strip_1,Strip
+28,l4,master_bedroom,suite,door,1,4,master_bedroom_suite,l_master_bedroom_suite,master_bedroom_suite,input_boolean.master_bedroom_suite_door_1,Door
+29,l4,master_bedroom,suite,dresser,1,5,master_bedroom_suite,l_master_bedroom_suite,master_bedroom_suite,input_boolean.master_bedroom_suite_dresser_1,Dresser
+30,l4,master_bedroom,bathroom,spots,1,1,master_bedroom_bathroom,l_master_bedroom_bathroom,master_bedroom_bathroom,input_boolean.master_bedroom_bathroom_spots_1,bathroom spots
+31,l4,master_bedroom,bathroom,fan,1,2,master_bedroom_bathroom,l_master_bedroom_bathroom,master_bedroom_bathroom,input_boolean.master_bedroom_bathroom_fan_1,bathroom fan
+32,l5,exterior,veranda,main,1,1,exterior_veranda,l_exterior_veranda,exterior_veranda,input_boolean.exterior_veranda_main_1,Veranda
+33,l5,exterior,yard,bbq,1,1,exterior_yard,l_exterior_yard,exterior_yard,input_boolean.exterior_yard_bbq_1,BBQ
+34,l5,exterior,entrance,door,1,1,exterior_entrance,l_exterior_entrance,exterior_entrance,input_boolean.exterior_entrance_door_1,Entrance Door
+35,l5,exterior,yard,front_lights,1,2,exterior_yard,l_exterior_yard,exterior_yard,input_boolean.exterior_yard_front_lights_1,Front Lights
+36,l5,exterior,yard,backyard_flood_light,1,3,exterior_yard,l_exterior_yard,exterior_yard,input_boolean.exterior_yard_backyard_flood_light_1,backyard flood light 1
+37,l5,exterior,yard,backyard_flood_light,2,4,exterior_yard,l_exterior_yard,exterior_yard,input_boolean.exterior_yard_backyard_flood_light_2,backyard flood light 2
+38,l5,exterior,yard,master_bedroom_flood_light,1,5,exterior_yard,l_exterior_yard,exterior_yard,input_boolean.exterior_yard_master_bedroom_flood_light_1,Master Bedroom flood light
+39,l5,exterior,yard,driveway,1,6,exterior_yard,l_exterior_yard,exterior_yard,input_boolean.exterior_yard_driveway_1,Driveway
diff --git a/docs/Electrical/lights_zones_cleaned.csv b/docs/Electrical/lights_zones_cleaned.csv
new file mode 100644
index 0000000..15e01de
--- /dev/null
+++ b/docs/Electrical/lights_zones_cleaned.csv
@@ -0,0 +1,40 @@
+#,zone,area,light
+1,l1,open_plan,living_room_main
+2,l1,open_plan,living_room_spots
+3,l1,open_plan,dining_room_main
+4,l1,open_plan,dining_room_spots
+5,l1,open_plan,entrance_mirror
+6,l1,open_plan,entrance_main
+7,l1,open_plan,guest_wc_main
+8,l1,open_plan,guest_wc_strip
+9,l1,open_plan,guest_wc_fan
+10,l1,open_plan,living_room_strip
+11,l1,open_plan,living_room_strip
+12,l2,kitchen,kitchen_main
+13,l2,kitchen,island
+14,l2,kitchen,kitchen_cabinets
+15,l2,kitchen,fridge
+16,l2,kitchen,pantry
+17,l3,bedrooms,office_main
+18,l3,bedrooms,office_strip
+19,l3,bedrooms,hallway
+20,l3,bedrooms,laundry
+21,l3,bedrooms,shower_spots
+22,l3,bedrooms,shower_fan
+23,l3,bedrooms,bedroom_main
+24,l3,bedrooms,bedroom_spots
+25,l4,master_bedroom,main
+26,l4,master_bedroom,spots
+27,l4,master_bedroom,strip
+28,l4,master_bedroom,door
+29,l4,master_bedroom,dresser
+30,l4,master_bedroom,bathroom_spots
+31,l4,master_bedroom,bathroom_fan
+32,l5,exterior,veranda
+33,l5,exterior,bbq
+34,l5,exterior,entrance_door
+35,l5,exterior,front_lights
+36,l5,exterior,backyard_flood_light_1
+37,l5,exterior,backyard_flood_light_2
+38,l5,exterior,master_bedroom_flood_light
+39,l5,exterior,driveway
diff --git a/docs/Electrical/naming_rules.md b/docs/Electrical/naming_rules.md
new file mode 100644
index 0000000..762a93b
--- /dev/null
+++ b/docs/Electrical/naming_rules.md
@@ -0,0 +1,70 @@
+# Uniform lighting naming rules
+
+This project uses one canonical naming strategy for CODESYS, Node-RED, and Home Assistant.
+
+## 1) Canonical token format
+
+- All tokens are `snake_case`.
+- Allowed chars: `a-z`, `0-9`, `_`.
+- Normalize free text by:
+ - lowercasing
+ - trimming spaces
+ - replacing spaces and dashes with `_`
+ - collapsing repeated `_`
+ - applying typo fixes (`ktchen -> kitchen`, `guiest -> guest`, `wachine -> laundry`)
+
+## 2) Canonical data columns
+
+Source of truth is `lights_zones_canonical.csv` with:
+
+- `circuit_no`
+- `zone`
+- `area`
+- `room`
+- `light_type`
+- `index` (duplicate counter for same area+room+light_type)
+- `light_no` (PLC light channel inside room, 1..6)
+- `codesys_in_key`
+- `codesys_out_key`
+- `nodered_room_key`
+- `ha_entity_id`
+
+## 3) ID generation formulas
+
+- `codesys_in_key` = `_`
+- `codesys_out_key` = `l__`
+- `nodered_room_key` = `_`
+- `ha_entity_id` = `input_boolean.___`
+
+Examples:
+
+- `input_boolean.open_plan_living_room_main_1`
+- `open_plan_guest_wc`
+- `l_master_bedroom_suite`
+
+## 4) PLC mapping rule
+
+- `light_no` maps to CODESYS `l_` and Node-RED `light`:
+ - `light_no = 1` -> `l_1`
+ - `light_no = 2` -> `l_2`
+ - ...
+ - `light_no = 6` -> `l_6`
+
+## 5) Reserved room keys used in this migration
+
+- `open_plan_living_room`
+- `open_plan_dining_room`
+- `open_plan_entrance`
+- `open_plan_guest_wc`
+- `kitchen_kitchen`
+- `kitchen_pantry`
+- `bedrooms_office`
+- `bedrooms_hallway`
+- `bedrooms_laundry`
+- `bedrooms_shower`
+- `bedrooms_bedroom`
+- `master_bedroom_suite`
+- `master_bedroom_bathroom`
+- `exterior_veranda`
+- `exterior_entrance`
+- `exterior_yard`
diff --git a/docs/Electrical/validation_report.md b/docs/Electrical/validation_report.md
new file mode 100644
index 0000000..924b2a4
--- /dev/null
+++ b/docs/Electrical/validation_report.md
@@ -0,0 +1,37 @@
+# Validation report (uniform lighting naming migration)
+
+## Static consistency checks completed
+
+- Canonical lighting rows: `39` (`lights_zones_canonical.csv`)
+- CODESYS `NVL_In` room structs: `16`
+- CODESYS `NVL_Out` room structs: `16`
+- Node-RED `roomNames` entries: `16`
+- Node-RED `lightEntityMap` entries: `39`
+
+## Mapping status
+
+- `circuit_no -> area/room/type/index/light_no` is defined for all non-empty circuits.
+- `codesys_in_key` / `codesys_out_key` are defined for every canonical row.
+- `room-config.js` is aligned with canonical keys:
+ - `roomNames` matches the 16-room CODESYS layout.
+ - `lightEntityMap` has one entry per canonical row.
+ - `entityToRoom` base keys map all canonical HA entity patterns.
+
+## Verified constraints
+
+- Canonical naming style: `snake_case`
+- Entity ID pattern: `input_boolean.___`
+- CODESYS legacy room names replaced in `codesys/src` runtime files:
+ - `GVL_NVL_placeholders.gvl`
+ - `GVL_IO.gvl`
+ - `PLC_App.st`
+
+## Residual runtime checks to perform on target system
+
+Because this repository environment has no runtime binaries for Node-RED/CODESYS/HA validation, perform these on your deployed setup:
+
+1. Deploy updated CODESYS project and confirm NVL sender/receiver symbol binding still resolves.
+2. Load updated `/root/.node-red/room-config.js` and redeploy flows.
+3. Import HA helpers from `lights_by_area_room_type_index.yaml`.
+4. Test one circuit per area end-to-end:
+ - HA toggle -> Node-RED -> NVL_In -> PLC output -> NVL_Out -> HA sync.
diff --git a/docs/integration/all-lights-and-rooms.md b/docs/integration/all-lights-and-rooms.md
index 6e5489b..8f2df13 100644
--- a/docs/integration/all-lights-and-rooms.md
+++ b/docs/integration/all-lights-and-rooms.md
@@ -10,6 +10,11 @@ To avoid adding a new room or light in several places, use **one config file** t
**Config file:** [`node-red/config_files/room-config.js`](../../node-red/config_files/room-config.js) — exports `ROOM_CONFIG` with: **roomNames** (NVL SEND), **lightEntityMap** (NVL to HA Sync), **entityToRoom** (HA to NVL), **deviceToRoom** (Zigbee to NVL).
+Canonical source-of-truth for this migration:
+
+- Electrical mapping: [`../Electrical/lights_zones_canonical.csv`](../Electrical/lights_zones_canonical.csv)
+- Naming rules: [`../Electrical/naming_rules.md`](../Electrical/naming_rules.md)
+
**Server location:** `/root/.node-red/room-config.js` (uploaded via `scp`).
**Loader:** [`node-red/room-config-loader.js`](../../node-red/room-config-loader.js) — paste this code into a **Function node** named "Load room config". Connect an **Inject node** (once after 0.5 s) to it and deploy. It `require()`s the config file, clears the cache (so edits are picked up on redeploy), and sets `global.set('roomConfig', ...)`.
@@ -39,14 +44,14 @@ Use a single list of room keys everywhere. Example (match your CODESYS NVL order
| Room key (Node-RED / NVL) | PLC NVL_Out (receive) | Lights per room |
|---------------------------|------------------------|-----------------|
-| `livingRoom` | `l_livingRoom` | 1..6 |
-| `masterBedroom` | `l_masterBedroom` | 1..6 |
-| `kitchen` | `l_kitchen` | 1..6 |
-| `bathroom` | `l_bathroom` | 1..6 |
+| `open_plan_living_room` | `l_open_plan_living_room` | 1..6 |
+| `open_plan_guest_wc` | `l_open_plan_guest_wc` | 1..6 |
+| `kitchen_kitchen` | `l_kitchen_kitchen` | 1..6 |
+| `master_bedroom_suite` | `l_master_bedroom_suite` | 1..6 |
| … | … | 1..6 |
-- **NVL send (to PLC):** same keys as NVL_In in CODESYS (e.g. `livingRoom`, `masterBedroom`, … or `cmd_livingroom` for the test slot). Order and names must match the PLC.
-- **NVL receive (from PLC):** keys in the nvl-receive output (e.g. `l_livingRoom`, `light_livingRoom`) — use the same key in **LIGHT_ENTITY_MAP** in the sync function.
+- **NVL send (to PLC):** same keys as NVL_In in CODESYS (e.g. `open_plan_living_room`, `master_bedroom_suite`). Order and names must match the PLC.
+- **NVL receive (from PLC):** keys in the nvl-receive output (e.g. `l_open_plan_living_room`) — use the same key in **LIGHT_ENTITY_MAP** in the sync function.
Keep one list (e.g. in a comment or config file) and use it in:
- **state-to-nvl-send-payload.js** → `roomNames`
@@ -254,9 +259,9 @@ Add or remove lines to match your rooms and light counts. Entity IDs become `inp
Use a **consistent pattern** so you can derive room + light in Node-RED and in YAML:
-- **Entity ID:** `input_boolean._`
- Examples: `living_room_1`, `kitchen_2`, `master_bedroom_1`.
-- Then in HA to NVL you can parse: room from the first part, light from the trailing number.
+- **Entity ID:** `input_boolean.___`
+ Examples: `input_boolean.open_plan_living_room_main_1`, `input_boolean.kitchen_kitchen_island_1`, `input_boolean.exterior_yard_driveway_1`.
+- In HA to NVL, parse room mapping from `entityToRoom` using the base without the trailing `_`.
---
diff --git a/docs/integration/ha-lights-and-rooms.md b/docs/integration/ha-lights-and-rooms.md
index 3c3494b..7c56d91 100644
--- a/docs/integration/ha-lights-and-rooms.md
+++ b/docs/integration/ha-lights-and-rooms.md
@@ -2,6 +2,15 @@
This guide covers how to create the light/switch entities that Node-RED and the PLC use, and how to assign them to **rooms** (HA **Areas**) so you can manage and display them by room.
+Canonical naming in this project is now:
+
+- `input_boolean.___`
+- Example: `input_boolean.open_plan_living_room_main_1`
+
+Pre-generated helper YAML for the current electrical plan:
+
+- [`lights_by_area_room_type_index.yaml`](lights_by_area_room_type_index.yaml)
+
---
## 1. Concepts
@@ -41,18 +50,13 @@ Create one file per room (or one file with sections) so entities are grouped by
```yaml
# PLC/Node-RED lights – entity_id must match room-config.js (entityToRoom, lightEntityMap)
-# Naming: _ e.g. living_room_1, kitchen_2
+# Naming: ___ e.g. open_plan_living_room_main_1
input_boolean:
- # Living room
- living_room_1: { name: Living Room 1 }
- living_room_2: { name: Living Room 2 }
- # Kitchen
- kitchen_1: { name: Kitchen 1 }
- kitchen_2: { name: Kitchen 2 }
- # Hallway
- hallway_1: { name: Hallway 1 }
- # Add more: master_bedroom_1, bathroom_1, entrance_1, etc.
+ open_plan_living_room_main_1: { name: Open Plan Living Room Main 1 }
+ open_plan_living_room_spots_1: { name: Open Plan Living Room Spots 1 }
+ kitchen_kitchen_main_1: { name: Kitchen Main 1 }
+ bedrooms_hallway_main_1: { name: Bedrooms Hallway Main 1 }
```
Ensure `configuration.yaml` has:
@@ -62,7 +66,7 @@ homeassistant:
packages: !include_dir_named packages
```
-Restart Home Assistant. All entities are created. Entity IDs will be `input_boolean.living_room_1`, etc.
+Restart Home Assistant. All entities are created. Entity IDs will be `input_boolean.open_plan_living_room_main_1`, etc.
### Option B: UI (one by one)
@@ -99,9 +103,9 @@ Use the **same entity_id and room logic** in both places.
**Naming convention that works well:**
-- Entity ID: `input_boolean._`
- Examples: `living_room_1`, `kitchen_2`, `hallway_1`.
-- In **entityToRoom** use the part without the number: `living_room`, `kitchen`, `hallway` (and any alias like `living_room_new` → `cmd_livingroom`).
+- Entity ID: `input_boolean.___`
+ Examples: `open_plan_living_room_main_1`, `kitchen_kitchen_island_1`, `exterior_yard_driveway_1`.
+- In **entityToRoom** use the part without trailing `_`, e.g. `open_plan_living_room_main` or `exterior_yard_driveway`.
When you add a new light in HA (new entity_id), add or update:
diff --git a/docs/integration/lights_by_area_room_type_index.yaml b/docs/integration/lights_by_area_room_type_index.yaml
new file mode 100644
index 0000000..04254b9
--- /dev/null
+++ b/docs/integration/lights_by_area_room_type_index.yaml
@@ -0,0 +1,47 @@
+# Home Assistant helpers for the canonical lighting naming model.
+# Naming: input_boolean.___
+
+input_boolean:
+ open_plan_living_room_main_1: { name: Open Plan Living Room Main 1 }
+ open_plan_living_room_spots_1: { name: Open Plan Living Room Spots 1 }
+ open_plan_dining_room_main_1: { name: Open Plan Dining Room Main 1 }
+ open_plan_dining_room_spots_1: { name: Open Plan Dining Room Spots 1 }
+ open_plan_entrance_mirror_1: { name: Open Plan Entrance Mirror 1 }
+ open_plan_entrance_main_1: { name: Open Plan Entrance Main 1 }
+ open_plan_guest_wc_main_1: { name: Open Plan Guest WC Main 1 }
+ open_plan_guest_wc_strip_1: { name: Open Plan Guest WC Strip 1 }
+ open_plan_guest_wc_fan_1: { name: Open Plan Guest WC Fan 1 }
+ open_plan_living_room_strip_1: { name: Open Plan Living Room Strip 1 }
+ open_plan_living_room_strip_2: { name: Open Plan Living Room Strip 2 }
+
+ kitchen_kitchen_main_1: { name: Kitchen Main 1 }
+ kitchen_kitchen_island_1: { name: Kitchen Island 1 }
+ kitchen_kitchen_cabinets_1: { name: Kitchen Cabinets 1 }
+ kitchen_kitchen_fridge_1: { name: Kitchen Fridge 1 }
+ kitchen_pantry_main_1: { name: Kitchen Pantry Main 1 }
+
+ bedrooms_office_main_1: { name: Bedrooms Office Main 1 }
+ bedrooms_office_strip_1: { name: Bedrooms Office Strip 1 }
+ bedrooms_hallway_main_1: { name: Bedrooms Hallway Main 1 }
+ bedrooms_laundry_main_1: { name: Bedrooms Laundry Main 1 }
+ bedrooms_shower_spots_1: { name: Bedrooms Shower Spots 1 }
+ bedrooms_shower_fan_1: { name: Bedrooms Shower Fan 1 }
+ bedrooms_bedroom_main_1: { name: Bedrooms Bedroom Main 1 }
+ bedrooms_bedroom_spots_1: { name: Bedrooms Bedroom Spots 1 }
+
+ master_bedroom_suite_main_1: { name: Master Bedroom Suite Main 1 }
+ master_bedroom_suite_spots_1: { name: Master Bedroom Suite Spots 1 }
+ master_bedroom_suite_strip_1: { name: Master Bedroom Suite Strip 1 }
+ master_bedroom_suite_door_1: { name: Master Bedroom Suite Door 1 }
+ master_bedroom_suite_dresser_1: { name: Master Bedroom Suite Dresser 1 }
+ master_bedroom_bathroom_spots_1: { name: Master Bedroom Bathroom Spots 1 }
+ master_bedroom_bathroom_fan_1: { name: Master Bedroom Bathroom Fan 1 }
+
+ exterior_veranda_main_1: { name: Exterior Veranda Main 1 }
+ exterior_entrance_door_1: { name: Exterior Entrance Door 1 }
+ exterior_yard_bbq_1: { name: Exterior Yard BBQ 1 }
+ exterior_yard_front_lights_1: { name: Exterior Yard Front Lights 1 }
+ exterior_yard_backyard_flood_light_1: { name: Exterior Yard Backyard Flood Light 1 }
+ exterior_yard_backyard_flood_light_2: { name: Exterior Yard Backyard Flood Light 2 }
+ exterior_yard_master_bedroom_flood_light_1: { name: Exterior Yard Master Bedroom Flood Light 1 }
+ exterior_yard_driveway_1: { name: Exterior Yard Driveway 1 }
diff --git a/node-red/config_files/flows.json b/node-red/config_files/flows.json
index 9f72d9c..6c9ef59 100644
--- a/node-red/config_files/flows.json
+++ b/node-red/config_files/flows.json
@@ -6297,7 +6297,7 @@
"type": "function",
"z": "7de41d810b04d992",
"name": "HA to NVL",
- "func": "// Writes to state.rooms.. Room from entity_id via global roomConfig.entityToRoom. See /root/.node-red/room-config.js.\nconst entityId = (msg.topic || '').toString();\nconst config = global.get('roomConfig');\nconst entityToRoom = (config && config.entityToRoom) ? config.entityToRoom : { living_room_new: 'cmd_livingroom' };\n// Parse entity_id e.g. input_boolean.living_room_1 → base \"living_room_1\", then room + light\nconst base = (entityId.split('.')[1] || '').replace(/\\s/g, '');\nconst numMatch = base.match(/_(\\d+)$/);\nconst lightNum = (numMatch && parseInt(numMatch[1], 10) >= 1) ? Math.min(6, parseInt(numMatch[1], 10)) : 1;\nconst baseWithoutNum = base.replace(/_?\\d+$/, '') || base;\nconst ROOM_NAME = entityToRoom[baseWithoutNum] || entityToRoom[base] || 'cmd_livingroom';\n\n// Normalize state: HA / trigger-state can send payload as string, object with .state, or data.new_state.state\nlet rawState = msg.payload;\nif (rawState !== null && typeof rawState === 'object' && rawState.state !== undefined) rawState = rawState.state;\nif (msg.data && msg.data.new_state && msg.data.new_state.state !== undefined) rawState = msg.data.new_state.state;\nconst isOn = (rawState === 'on' || rawState === true);\n\nnode.warn('[HA to NVL] topic=' + entityId + ' rawState=' + JSON.stringify(rawState) + ' → lightNum=' + lightNum + ' isOn=' + isOn);\n\nif (!flow.get('nvlInState')) flow.set('nvlInState', { rooms: {}, boiler: {} });\nconst state = flow.get('nvlInState');\nif (!state.rooms[ROOM_NAME]) state.rooms[ROOM_NAME] = {};\nconst r = state.rooms[ROOM_NAME];\nr['ha_l' + lightNum + '_on'] = false;\nr['ha_l' + lightNum + '_off'] = false;\nif (isOn) r['ha_l' + lightNum + '_on'] = true;\nelse r['ha_l' + lightNum + '_off'] = true;\nflow.set('nvlInState', state);\n\nnode.warn('[HA to NVL] set ' + ROOM_NAME + ' ha_l' + lightNum + '_' + (isOn ? 'on' : 'off') + '=true, buildAndSend');\n\n// PLC uses R_TRIG (rising edge). If \"off\" still doesn’t work, add a flow: after this node, 80ms delay then clear ha_l*_off and buildAndSend again (pulse).\nmsg.payload = { buildAndSend: true };\nreturn msg;\n",
+ "func": "// Writes to state.rooms.. Room from entity_id via global roomConfig.entityToRoom. See /root/.node-red/room-config.js.\nconst entityId = (msg.topic || '').toString();\nconst config = global.get('roomConfig');\nconst entityToRoom = (config && config.entityToRoom) ? config.entityToRoom : { open_plan_living_room_main: 'open_plan_living_room' };\n// Parse entity_id e.g. input_boolean.open_plan_living_room_main_1 → base \"open_plan_living_room_main_1\", then room + light\nconst base = (entityId.split('.')[1] || '').replace(/\\s/g, '');\nconst numMatch = base.match(/_(\\d+)$/);\nconst lightNum = (numMatch && parseInt(numMatch[1], 10) >= 1) ? Math.min(6, parseInt(numMatch[1], 10)) : 1;\nconst baseWithoutNum = base.replace(/_?\\d+$/, '') || base;\nconst ROOM_NAME = entityToRoom[baseWithoutNum] || entityToRoom[base] || 'open_plan_living_room';\n\n// Normalize state: HA / trigger-state can send payload as string, object with .state, or data.new_state.state\nlet rawState = msg.payload;\nif (rawState !== null && typeof rawState === 'object' && rawState.state !== undefined) rawState = rawState.state;\nif (msg.data && msg.data.new_state && msg.data.new_state.state !== undefined) rawState = msg.data.new_state.state;\nconst isOn = (rawState === 'on' || rawState === true);\n\nnode.warn('[HA to NVL] topic=' + entityId + ' rawState=' + JSON.stringify(rawState) + ' → lightNum=' + lightNum + ' isOn=' + isOn);\n\nif (!flow.get('nvlInState')) flow.set('nvlInState', { rooms: {}, boiler: {} });\nconst state = flow.get('nvlInState');\nif (!state.rooms[ROOM_NAME]) state.rooms[ROOM_NAME] = {};\nconst r = state.rooms[ROOM_NAME];\nr['ha_l' + lightNum + '_on'] = false;\nr['ha_l' + lightNum + '_off'] = false;\nif (isOn) r['ha_l' + lightNum + '_on'] = true;\nelse r['ha_l' + lightNum + '_off'] = true;\nflow.set('nvlInState', state);\n\nnode.warn('[HA to NVL] set ' + ROOM_NAME + ' ha_l' + lightNum + '_' + (isOn ? 'on' : 'off') + '=true, buildAndSend');\n\n// PLC uses R_TRIG (rising edge). If \"off\" still doesn’t work, add a flow: after this node, 80ms delay then clear ha_l*_off and buildAndSend again (pulse).\nmsg.payload = { buildAndSend: true };\nreturn msg;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
@@ -6317,7 +6317,7 @@
"type": "function",
"z": "7de41d810b04d992",
"name": "Zigbee to NVL",
- "func": "// Each button maps to (room, light) via global roomConfig.switchBindings. See /root/.node-red/room-config.js.\n// Supports single-device payload (action, friendly_name at top level) and multi-device payload (keyed by IEEE address).\nconst actionToSwitch = { single: 1, double: 2, hold: 3, release: 4, triple: 5, quad: 6 };\nconst config = global.get('roomConfig');\nconst switchBindings = (config && config.switchBindings) ? config.switchBindings : {};\nconst deviceToRoom = (config && config.deviceToRoom) ? config.deviceToRoom : { 'Office Switch': 'cmd_livingroom' };\n// Optional: map IEEE address to friendly name when multi-device node doesn't provide it. e.g. deviceIdToName: { '0xa4c138a5b9771b05': 'Office Switch' }\nconst deviceIdToName = (config && config.deviceIdToName) ? config.deviceIdToName : {};\n\nconst payload = msg.payload != null ? msg.payload : {};\n// Payload can be: single-device { action, friendly_name }, multi-device { \"0x...\": { ... } }, or plain action string \"1_single\"\nlet friendlyName = (typeof payload === 'object' && payload.friendly_name != null) ? payload.friendly_name.toString() : '';\nlet actionRaw = (typeof payload === 'string' ? payload : (payload.action || payload.click || msg.action || (payload && payload.action)) || '').toString().toLowerCase();\n\n// Multi-device payload: prefer the device that has an action AND a configured binding (so we don't pick another device's stale action)\nif (!actionRaw && typeof payload === 'object' && !Array.isArray(payload)) {\n const keys = Object.keys(payload);\n let fallback = null;\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n const v = payload[key];\n if (key && key.toLowerCase().indexOf('0x') === 0 && v && typeof v === 'object') {\n const inner = v.payload || v.message || v.state || v;\n const a = (inner.action || inner.click || v.action || v.click || '').toString().toLowerCase();\n if (!a) continue;\n const name = (v.friendly_name || v.name || (v.item && (v.item.friendly_name || v.item.name)) || deviceIdToName[key] || key).toString();\n if (switchBindings[name] || deviceToRoom[name]) {\n actionRaw = a;\n friendlyName = name;\n break;\n }\n if (!fallback) fallback = { actionRaw: a, friendlyName: name };\n }\n }\n if (!actionRaw && fallback) {\n actionRaw = fallback.actionRaw;\n friendlyName = fallback.friendlyName;\n }\n}\n// If still no action, try msg.data or msg.topic (some nodes put action in topic, e.g. \"1_single\")\nif (!actionRaw && msg.data && msg.data.action) actionRaw = msg.data.action.toString().toLowerCase();\nif (!actionRaw && typeof msg.topic === 'string' && /^\\d_[a-z_]+$/.test(msg.topic.trim())) actionRaw = msg.topic.trim().toLowerCase();\n// If payload was the action string, get friendly_name from topic e.g. zigbee2mqtt/Office Switch/action\nif (actionRaw && !friendlyName && typeof msg.topic === 'string') {\n const parts = msg.topic.split('/');\n if (parts.length >= 2) friendlyName = parts[1].trim(); // zigbee2mqtt/Office Switch/action → Office Switch\n}\n\nif (!actionRaw) {\n node.warn('[Zigbee to NVL] no action in payload. Keys: ' + (typeof payload === 'object' ? Object.keys(payload).join(', ') : 'n/a'));\n return null;\n}\n\nconst parts = actionRaw.split('_');\nlet buttonIndex = null;\nif (parts.length >= 2) {\n const n = parseInt(parts[0], 10);\n if (n >= 1 && n <= 6) buttonIndex = n;\n}\nif (buttonIndex == null) buttonIndex = actionToSwitch[actionRaw];\nif (buttonIndex == null && parts.length >= 2) buttonIndex = actionToSwitch[parts.slice(1).join('_')];\nif (buttonIndex == null) buttonIndex = actionToSwitch[parts[parts.length - 1]];\n\nif (buttonIndex == null) {\n node.warn('[Zigbee to NVL] unknown action: ' + JSON.stringify(actionRaw));\n return null;\n}\n\nnode.warn('[Zigbee to NVL] action=' + actionRaw + ' buttonIndex=' + buttonIndex + ' device=' + friendlyName);\n\n// Prefer switchBindings by friendly name; fallback by device ID (IEEE) if we have switchBindingsByDeviceId\nconst bindingByDeviceId = (config && config.switchBindingsByDeviceId) ? config.switchBindingsByDeviceId : null;\nlet targets = [];\nlet binding = switchBindings[friendlyName] && switchBindings[friendlyName][buttonIndex];\nif ((binding === undefined || binding === null) && bindingByDeviceId) {\n const deviceId = typeof payload === 'object' && Object.keys(payload).some(function (k) { return k.indexOf('0x') === 0; })\n ? Object.keys(payload).find(function (k) { return k.indexOf('0x') === 0; })\n : null;\n if (deviceId) binding = bindingByDeviceId[deviceId] && bindingByDeviceId[deviceId][buttonIndex];\n}\nif (binding !== undefined && binding !== null) {\n targets = Array.isArray(binding) ? binding : [binding];\n targets = targets.filter(function (t) { return t && t.room && t.light >= 1 && t.light <= 6; });\n}\nif (targets.length === 0) {\n const room = deviceToRoom[friendlyName] || 'cmd_livingroom';\n targets = [{ room: room, light: buttonIndex }];\n}\n\nif (!flow.get('nvlInState')) flow.set('nvlInState', { rooms: {}, boiler: {} });\nconst state = flow.get('nvlInState');\nconst clearList = [];\n\nfor (const t of targets) {\n const key = 'zigbee_sw' + t.light;\n if (!state.rooms[t.room]) state.rooms[t.room] = {};\n state.rooms[t.room][key] = true;\n clearList.push({ room: t.room, key: key });\n}\n\nflow.set('nvlInState', state);\n\nnode.warn('[Zigbee to NVL] ' + (friendlyName || 'device') + ' btn' + buttonIndex + ' → ' + targets.map(function (t) { return t.room + ' L' + t.light; }).join(', '));\n\nmsg.payload = { buildAndSend: true };\nmsg.zigbeeClear = clearList;\nreturn msg;\n",
+ "func": "// Each button maps to (room, light) via global roomConfig.switchBindings. See /root/.node-red/room-config.js.\n// Supports single-device payload (action, friendly_name at top level) and multi-device payload (keyed by IEEE address).\nconst actionToSwitch = { single: 1, double: 2, hold: 3, release: 4, triple: 5, quad: 6 };\nconst config = global.get('roomConfig');\nconst switchBindings = (config && config.switchBindings) ? config.switchBindings : {};\nconst deviceToRoom = (config && config.deviceToRoom) ? config.deviceToRoom : { 'Office Switch': 'open_plan_living_room' };\n// Optional: map IEEE address to friendly name when multi-device node doesn't provide it. e.g. deviceIdToName: { '0xa4c138a5b9771b05': 'Office Switch' }\nconst deviceIdToName = (config && config.deviceIdToName) ? config.deviceIdToName : {};\n\nconst payload = msg.payload != null ? msg.payload : {};\n// Payload can be: single-device { action, friendly_name }, multi-device { \"0x...\": { ... } }, or plain action string \"1_single\"\nlet friendlyName = (typeof payload === 'object' && payload.friendly_name != null) ? payload.friendly_name.toString() : '';\nlet actionRaw = (typeof payload === 'string' ? payload : (payload.action || payload.click || msg.action || (payload && payload.action)) || '').toString().toLowerCase();\n\n// Multi-device payload: prefer the device that has an action AND a configured binding (so we don't pick another device's stale action)\nif (!actionRaw && typeof payload === 'object' && !Array.isArray(payload)) {\n const keys = Object.keys(payload);\n let fallback = null;\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n const v = payload[key];\n if (key && key.toLowerCase().indexOf('0x') === 0 && v && typeof v === 'object') {\n const inner = v.payload || v.message || v.state || v;\n const a = (inner.action || inner.click || v.action || v.click || '').toString().toLowerCase();\n if (!a) continue;\n const name = (v.friendly_name || v.name || (v.item && (v.item.friendly_name || v.item.name)) || deviceIdToName[key] || key).toString();\n if (switchBindings[name] || deviceToRoom[name]) {\n actionRaw = a;\n friendlyName = name;\n break;\n }\n if (!fallback) fallback = { actionRaw: a, friendlyName: name };\n }\n }\n if (!actionRaw && fallback) {\n actionRaw = fallback.actionRaw;\n friendlyName = fallback.friendlyName;\n }\n}\n// If still no action, try msg.data or msg.topic (some nodes put action in topic, e.g. \"1_single\")\nif (!actionRaw && msg.data && msg.data.action) actionRaw = msg.data.action.toString().toLowerCase();\nif (!actionRaw && typeof msg.topic === 'string' && /^\\d_[a-z_]+$/.test(msg.topic.trim())) actionRaw = msg.topic.trim().toLowerCase();\n// If payload was the action string, get friendly_name from topic e.g. zigbee2mqtt/Office Switch/action\nif (actionRaw && !friendlyName && typeof msg.topic === 'string') {\n const parts = msg.topic.split('/');\n if (parts.length >= 2) friendlyName = parts[1].trim(); // zigbee2mqtt/Office Switch/action → Office Switch\n}\n\nif (!actionRaw) {\n node.warn('[Zigbee to NVL] no action in payload. Keys: ' + (typeof payload === 'object' ? Object.keys(payload).join(', ') : 'n/a'));\n return null;\n}\n\nconst parts = actionRaw.split('_');\nlet buttonIndex = null;\nif (parts.length >= 2) {\n const n = parseInt(parts[0], 10);\n if (n >= 1 && n <= 6) buttonIndex = n;\n}\nif (buttonIndex == null) buttonIndex = actionToSwitch[actionRaw];\nif (buttonIndex == null && parts.length >= 2) buttonIndex = actionToSwitch[parts.slice(1).join('_')];\nif (buttonIndex == null) buttonIndex = actionToSwitch[parts[parts.length - 1]];\n\nif (buttonIndex == null) {\n node.warn('[Zigbee to NVL] unknown action: ' + JSON.stringify(actionRaw));\n return null;\n}\n\nnode.warn('[Zigbee to NVL] action=' + actionRaw + ' buttonIndex=' + buttonIndex + ' device=' + friendlyName);\n\n// Prefer switchBindings by friendly name; fallback by device ID (IEEE) if we have switchBindingsByDeviceId\nconst bindingByDeviceId = (config && config.switchBindingsByDeviceId) ? config.switchBindingsByDeviceId : null;\nlet targets = [];\nlet binding = switchBindings[friendlyName] && switchBindings[friendlyName][buttonIndex];\nif ((binding === undefined || binding === null) && bindingByDeviceId) {\n const deviceId = typeof payload === 'object' && Object.keys(payload).some(function (k) { return k.indexOf('0x') === 0; })\n ? Object.keys(payload).find(function (k) { return k.indexOf('0x') === 0; })\n : null;\n if (deviceId) binding = bindingByDeviceId[deviceId] && bindingByDeviceId[deviceId][buttonIndex];\n}\nif (binding !== undefined && binding !== null) {\n targets = Array.isArray(binding) ? binding : [binding];\n targets = targets.filter(function (t) { return t && t.room && t.light >= 1 && t.light <= 6; });\n}\nif (targets.length === 0) {\n const room = deviceToRoom[friendlyName] || 'open_plan_living_room';\n targets = [{ room: room, light: buttonIndex }];\n}\n\nif (!flow.get('nvlInState')) flow.set('nvlInState', { rooms: {}, boiler: {} });\nconst state = flow.get('nvlInState');\nconst clearList = [];\n\nfor (const t of targets) {\n const key = 'zigbee_sw' + t.light;\n if (!state.rooms[t.room]) state.rooms[t.room] = {};\n state.rooms[t.room][key] = true;\n clearList.push({ room: t.room, key: key });\n}\n\nflow.set('nvlInState', state);\n\nnode.warn('[Zigbee to NVL] ' + (friendlyName || 'device') + ' btn' + buttonIndex + ' → ' + targets.map(function (t) { return t.room + ' L' + t.light; }).join(', '));\n\nmsg.payload = { buildAndSend: true };\nmsg.zigbeeClear = clearList;\nreturn msg;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
@@ -6427,7 +6427,7 @@
"exposeAsEntityConfig": "",
"entities": {
"entity": [
- "input_boolean.living_room_new"
+ "input_boolean.open_plan_living_room_main_1"
],
"substring": [],
"regex": []
@@ -6452,7 +6452,7 @@
"type": "function",
"z": "7de41d810b04d992",
"name": "STATE TO NVL",
- "func": "// Build the payload for nvl-send from flow.nvlInState.\n// Room list comes from global roomConfig (single source of truth). See /root/.node-red/room-config.js.\n// Wire: HA to NVL / Zigbee to NVL → this function → nvl-send → udp out\n\nconst struct_room_cmds_default = {\n ha_l1_on: false, ha_l1_off: false, ha_l2_on: false, ha_l2_off: false,\n ha_l3_on: false, ha_l3_off: false, ha_l4_on: false, ha_l4_off: false,\n ha_l5_on: false, ha_l5_off: false, ha_l6_on: false, ha_l6_off: false,\n zigbee_sw1: false, zigbee_sw2: false, zigbee_sw3: false,\n zigbee_sw4: false, zigbee_sw5: false, zigbee_sw6: false,\n ha_all_on: false, ha_all_off: false\n};\n\nconst config = global.get('roomConfig');\nconst roomNames = (config && config.roomNames && config.roomNames.length) ? config.roomNames : ['cmd_livingroom'];\n\nconst state = flow.get('nvlInState') || { rooms: {}, boiler: {} };\nconst rooms = state.rooms || {};\nconst payload = {};\n\nfor (const name of roomNames) {\n const cmd = rooms[name] || {};\n payload[name] = { ...struct_room_cmds_default };\n for (const k of Object.keys(struct_room_cmds_default)) {\n if (cmd[k] !== undefined) payload[name][k] = !!cmd[k];\n }\n}\n\nmsg.payload = payload;\nreturn msg;\n",
+ "func": "// Build the payload for nvl-send from flow.nvlInState.\n// Room list comes from global roomConfig (single source of truth). See /root/.node-red/room-config.js.\n// Wire: HA to NVL / Zigbee to NVL → this function → nvl-send → udp out\n\nconst struct_room_cmds_default = {\n ha_l1_on: false, ha_l1_off: false, ha_l2_on: false, ha_l2_off: false,\n ha_l3_on: false, ha_l3_off: false, ha_l4_on: false, ha_l4_off: false,\n ha_l5_on: false, ha_l5_off: false, ha_l6_on: false, ha_l6_off: false,\n zigbee_sw1: false, zigbee_sw2: false, zigbee_sw3: false,\n zigbee_sw4: false, zigbee_sw5: false, zigbee_sw6: false,\n ha_all_on: false, ha_all_off: false\n};\n\nconst config = global.get('roomConfig');\nconst roomNames = (config && config.roomNames && config.roomNames.length) ? config.roomNames : ['open_plan_living_room'];\n\nconst state = flow.get('nvlInState') || { rooms: {}, boiler: {} };\nconst rooms = state.rooms || {};\nconst payload = {};\n\nfor (const name of roomNames) {\n const cmd = rooms[name] || {};\n payload[name] = { ...struct_room_cmds_default };\n for (const k of Object.keys(struct_room_cmds_default)) {\n if (cmd[k] !== undefined) payload[name][k] = !!cmd[k];\n }\n}\n\nmsg.payload = payload;\nreturn msg;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
@@ -6473,7 +6473,7 @@
"type": "function",
"z": "7de41d810b04d992",
"name": "NVL to HA Sync",
- "func": "// Run this AFTER nvl-receive. Syncs all mapped lights from PLC state to HA entities.\n// Map comes from global roomConfig (single source of truth). See /root/.node-red/room-config.js.\n// Connect output 1 → Action node (turn_on), output 2 → Action node (turn_off).\n\nconst config = global.get('roomConfig');\nconst LIGHT_ENTITY_MAP = (config && config.lightEntityMap && config.lightEntityMap.length) ? config.lightEntityMap : [\n { room: 'light_livingRoom', light: 1, entityId: 'input_boolean.living_room_new' },\n];\n\nconst payload = msg.payload || {};\nconst onMsgs = [];\nconst offMsgs = [];\n\nfor (const entry of LIGHT_ENTITY_MAP) {\n const room = payload[entry.room] || {};\n const isOn = !!(room['l_' + entry.light] || room['l' + entry.light]);\n\n const flowKey = 'nvlToHa_' + entry.entityId.replace(/\\./g, '_');\n const last = flow.get(flowKey);\n if (last === isOn) continue;\n flow.set(flowKey, isOn);\n\n const domain = entry.entityId.split('.')[0] || 'input_boolean';\n const action = domain + '.' + (isOn ? 'turn_on' : 'turn_off');\n const out = { payload: { action: action, target: { entity_id: [entry.entityId] } } };\n if (isOn) onMsgs.push(out); else offMsgs.push(out);\n}\n\nif (onMsgs.length === 0 && offMsgs.length === 0) return null;\nreturn [onMsgs, offMsgs];\n",
+ "func": "// Run this AFTER nvl-receive. Syncs all mapped lights from PLC state to HA entities.\n// Map comes from global roomConfig (single source of truth). See /root/.node-red/room-config.js.\n// Connect output 1 → Action node (turn_on), output 2 → Action node (turn_off).\n\nconst config = global.get('roomConfig');\nconst LIGHT_ENTITY_MAP = (config && config.lightEntityMap && config.lightEntityMap.length) ? config.lightEntityMap : [\n { room: 'l_open_plan_living_room', light: 1, entityId: 'input_boolean.open_plan_living_room_main_1' },\n];\n\nconst payload = msg.payload || {};\nconst onMsgs = [];\nconst offMsgs = [];\n\nfor (const entry of LIGHT_ENTITY_MAP) {\n const room = payload[entry.room] || {};\n const isOn = !!(room['l_' + entry.light] || room['l' + entry.light]);\n\n const flowKey = 'nvlToHa_' + entry.entityId.replace(/\\./g, '_');\n const last = flow.get(flowKey);\n if (last === isOn) continue;\n flow.set(flowKey, isOn);\n\n const domain = entry.entityId.split('.')[0] || 'input_boolean';\n const action = domain + '.' + (isOn ? 'turn_on' : 'turn_off');\n const out = { payload: { action: action, target: { entity_id: [entry.entityId] } } };\n if (isOn) onMsgs.push(out); else offMsgs.push(out);\n}\n\nif (onMsgs.length === 0 && offMsgs.length === 0) return null;\nreturn [onMsgs, offMsgs];\n",
"outputs": 2,
"timeout": 0,
"noerr": 0,
@@ -6728,7 +6728,7 @@
"z": "7de41d810b04d992",
"d": true,
"name": "NVL SEND",
- "func": "// Build the payload for nvl-send from flow.nvlInState.\n// Redesign: all rooms use struct_room_cmds. Add more names to roomNames when you add rooms.\n// Wire: HA to NVL / Zigbee to NVL → this function → nvl-send → udp out\n\nconst struct_room_cmds_default = {\n ha_l1_on: false, ha_l1_off: false, ha_l2_on: false, ha_l2_off: false,\n ha_l3_on: false, ha_l3_off: false, ha_l4_on: false, ha_l4_off: false,\n ha_l5_on: false, ha_l5_off: false, ha_l6_on: false, ha_l6_off: false,\n zigbee_sw1: false, zigbee_sw2: false, zigbee_sw3: false,\n zigbee_sw4: false, zigbee_sw5: false, zigbee_sw6: false,\n ha_all_on: false, ha_all_off: false\n};\n\n// NVL variable names per room (struct_room_cmds). Add more when you add rooms.\nconst roomNames = ['cmd_livingroom'];\n\nconst state = flow.get('nvlInState') || { rooms: {}, boiler: {} };\nconst rooms = state.rooms || {};\nconst payload = {};\n\nfor (const name of roomNames) {\n const cmd = rooms[name] || {};\n payload[name] = { ...struct_room_cmds_default };\n for (const k of Object.keys(struct_room_cmds_default)) {\n if (cmd[k] !== undefined) payload[name][k] = !!cmd[k];\n }\n}\n\nmsg.payload = payload;\nreturn msg;\n",
+ "func": "// Build the payload for nvl-send from flow.nvlInState.\n// Redesign: all rooms use struct_room_cmds. Add more names to roomNames when you add rooms.\n// Wire: HA to NVL / Zigbee to NVL → this function → nvl-send → udp out\n\nconst struct_room_cmds_default = {\n ha_l1_on: false, ha_l1_off: false, ha_l2_on: false, ha_l2_off: false,\n ha_l3_on: false, ha_l3_off: false, ha_l4_on: false, ha_l4_off: false,\n ha_l5_on: false, ha_l5_off: false, ha_l6_on: false, ha_l6_off: false,\n zigbee_sw1: false, zigbee_sw2: false, zigbee_sw3: false,\n zigbee_sw4: false, zigbee_sw5: false, zigbee_sw6: false,\n ha_all_on: false, ha_all_off: false\n};\n\n// NVL variable names per room (struct_room_cmds). Add more when you add rooms.\nconst roomNames = ['open_plan_living_room'];\n\nconst state = flow.get('nvlInState') || { rooms: {}, boiler: {} };\nconst rooms = state.rooms || {};\nconst payload = {};\n\nfor (const name of roomNames) {\n const cmd = rooms[name] || {};\n payload[name] = { ...struct_room_cmds_default };\n for (const k of Object.keys(struct_room_cmds_default)) {\n if (cmd[k] !== undefined) payload[name][k] = !!cmd[k];\n }\n}\n\nmsg.payload = payload;\nreturn msg;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
@@ -6749,7 +6749,7 @@
"z": "7de41d810b04d992",
"d": true,
"name": "NVL to HA Sync",
- "func": "// Run this AFTER nvl-receive. Syncs all mapped lights from PLC state to HA entities.\n// Connect output 1 → api-call-service (input_boolean.turn_on / light.turn_on), output 2 → api-call-service (turn_off).\n// Only outputs when state changed (per entity). Add more entries to LIGHT_ENTITY_MAP as you add rooms/lights.\n\n// Map: NVL room key (in payload) + light index 1..6 → HA entity_id\nconst LIGHT_ENTITY_MAP = [\n { room: 'light_livingRoom', light: 1, entityId: 'input_boolean.living_room_new' },\n // { room: 'light_livingRoom', light: 2, entityId: 'input_boolean.living_room_2' },\n // { room: 'l_kitchen', light: 1, entityId: 'light.kitchen_ceiling' },\n];\n\nconst payload = msg.payload || {};\nconst onMsgs = [];\nconst offMsgs = [];\n\nfor (const entry of LIGHT_ENTITY_MAP) {\n const room = payload[entry.room] || {};\n const isOn = !!(room['l_' + entry.light] || room['l' + entry.light]);\n\n const flowKey = 'nvlToHa_' + entry.entityId.replace(/\\./g, '_');\n const last = flow.get(flowKey);\n if (last === isOn) continue;\n flow.set(flowKey, isOn);\n\n const out = { payload: { entity_id: entry.entityId } };\n if (isOn) onMsgs.push(out); else offMsgs.push(out);\n}\n\nif (onMsgs.length === 0 && offMsgs.length === 0) return null;\nreturn [onMsgs, offMsgs];\n",
+ "func": "// Run this AFTER nvl-receive. Syncs all mapped lights from PLC state to HA entities.\n// Connect output 1 → api-call-service (input_boolean.turn_on / light.turn_on), output 2 → api-call-service (turn_off).\n// Only outputs when state changed (per entity). Add more entries to LIGHT_ENTITY_MAP as you add rooms/lights.\n\n// Map: NVL room key (in payload) + light index 1..6 → HA entity_id\nconst LIGHT_ENTITY_MAP = [\n { room: 'l_open_plan_living_room', light: 1, entityId: 'input_boolean.open_plan_living_room_main_1' },\n // { room: 'l_open_plan_living_room', light: 2, entityId: 'input_boolean.open_plan_living_room_spots_1' },\n // { room: 'l_kitchen_kitchen', light: 1, entityId: 'input_boolean.kitchen_kitchen_main_1' },\n];\n\nconst payload = msg.payload || {};\nconst onMsgs = [];\nconst offMsgs = [];\n\nfor (const entry of LIGHT_ENTITY_MAP) {\n const room = payload[entry.room] || {};\n const isOn = !!(room['l_' + entry.light] || room['l' + entry.light]);\n\n const flowKey = 'nvlToHa_' + entry.entityId.replace(/\\./g, '_');\n const last = flow.get(flowKey);\n if (last === isOn) continue;\n flow.set(flowKey, isOn);\n\n const out = { payload: { entity_id: entry.entityId } };\n if (isOn) onMsgs.push(out); else offMsgs.push(out);\n}\n\nif (onMsgs.length === 0 && offMsgs.length === 0) return null;\nreturn [onMsgs, offMsgs];\n",
"outputs": 2,
"timeout": 0,
"noerr": 0,
diff --git a/node-red/config_files/room-config.js b/node-red/config_files/room-config.js
index 622a941..e16b2e4 100644
--- a/node-red/config_files/room-config.js
+++ b/node-red/config_files/room-config.js
@@ -1,4 +1,4 @@
-ccc/**
+/**
* Single source of truth for rooms and lights.
* Lives on the Node-RED server at /root/.node-red/room-config.js
* All function nodes use: global.get('roomConfig')
@@ -9,56 +9,111 @@ ccc/**
const ROOM_CONFIG = {
// Order must match CODESYS NVL_In. Used by NVL SEND (state-to-nvl-send-payload).
roomNames: [
- 'cmd_livingroom',
- 'cmd_out', // add other NVL room keys as needed
- // 'masterBedroom',
- // 'masterBathroom',
- // 'bedroom_1',
- // 'bedroom_2',
- // 'bathroom',
- // 'guestWc',
- // 'kitchen',
- // 'pantry',
- // 'livingRoom',
- // 'dinningRoom',
- // 'entrance',
- // 'hallway',
- // 'outVeranda',
- // 'outFront',
- // 'outBack',
- // 'outSide',
+ 'open_plan_living_room',
+ 'open_plan_dining_room',
+ 'open_plan_entrance',
+ 'open_plan_guest_wc',
+ 'kitchen_kitchen',
+ 'kitchen_pantry',
+ 'bedrooms_office',
+ 'bedrooms_hallway',
+ 'bedrooms_laundry',
+ 'bedrooms_shower',
+ 'bedrooms_bedroom',
+ 'master_bedroom_suite',
+ 'master_bedroom_bathroom',
+ 'exterior_veranda',
+ 'exterior_entrance',
+ 'exterior_yard',
],
// PLC → HA sync. room = key in nvl-receive payload, light = 1..6, entityId = HA entity.
lightEntityMap: [
- { room: 'light_livingRoom', light: 1, entityId: 'input_boolean.living_room_new' },
- // { room: 'light_livingRoom', light: 2, entityId: 'input_boolean.living_room_2' },
- // { room: 'l_kitchen', light: 1, entityId: 'input_boolean.kitchen_1' },
+ { room: 'l_open_plan_living_room', light: 1, entityId: 'input_boolean.open_plan_living_room_main_1' },
+ { room: 'l_open_plan_living_room', light: 2, entityId: 'input_boolean.open_plan_living_room_spots_1' },
+ { room: 'l_open_plan_dining_room', light: 1, entityId: 'input_boolean.open_plan_dining_room_main_1' },
+ { room: 'l_open_plan_dining_room', light: 2, entityId: 'input_boolean.open_plan_dining_room_spots_1' },
+ { room: 'l_open_plan_entrance', light: 1, entityId: 'input_boolean.open_plan_entrance_mirror_1' },
+ { room: 'l_open_plan_entrance', light: 2, entityId: 'input_boolean.open_plan_entrance_main_1' },
+ { room: 'l_open_plan_guest_wc', light: 1, entityId: 'input_boolean.open_plan_guest_wc_main_1' },
+ { room: 'l_open_plan_guest_wc', light: 2, entityId: 'input_boolean.open_plan_guest_wc_strip_1' },
+ { room: 'l_open_plan_guest_wc', light: 3, entityId: 'input_boolean.open_plan_guest_wc_fan_1' },
+ { room: 'l_open_plan_living_room', light: 3, entityId: 'input_boolean.open_plan_living_room_strip_1' },
+ { room: 'l_open_plan_living_room', light: 4, entityId: 'input_boolean.open_plan_living_room_strip_2' },
+ { room: 'l_kitchen_kitchen', light: 1, entityId: 'input_boolean.kitchen_kitchen_main_1' },
+ { room: 'l_kitchen_kitchen', light: 2, entityId: 'input_boolean.kitchen_kitchen_island_1' },
+ { room: 'l_kitchen_kitchen', light: 3, entityId: 'input_boolean.kitchen_kitchen_cabinets_1' },
+ { room: 'l_kitchen_kitchen', light: 4, entityId: 'input_boolean.kitchen_kitchen_fridge_1' },
+ { room: 'l_kitchen_pantry', light: 1, entityId: 'input_boolean.kitchen_pantry_main_1' },
+ { room: 'l_bedrooms_office', light: 1, entityId: 'input_boolean.bedrooms_office_main_1' },
+ { room: 'l_bedrooms_office', light: 2, entityId: 'input_boolean.bedrooms_office_strip_1' },
+ { room: 'l_bedrooms_hallway', light: 1, entityId: 'input_boolean.bedrooms_hallway_main_1' },
+ { room: 'l_bedrooms_laundry', light: 1, entityId: 'input_boolean.bedrooms_laundry_main_1' },
+ { room: 'l_bedrooms_shower', light: 1, entityId: 'input_boolean.bedrooms_shower_spots_1' },
+ { room: 'l_bedrooms_shower', light: 2, entityId: 'input_boolean.bedrooms_shower_fan_1' },
+ { room: 'l_bedrooms_bedroom', light: 1, entityId: 'input_boolean.bedrooms_bedroom_main_1' },
+ { room: 'l_bedrooms_bedroom', light: 2, entityId: 'input_boolean.bedrooms_bedroom_spots_1' },
+ { room: 'l_master_bedroom_suite', light: 1, entityId: 'input_boolean.master_bedroom_suite_main_1' },
+ { room: 'l_master_bedroom_suite', light: 2, entityId: 'input_boolean.master_bedroom_suite_spots_1' },
+ { room: 'l_master_bedroom_suite', light: 3, entityId: 'input_boolean.master_bedroom_suite_strip_1' },
+ { room: 'l_master_bedroom_suite', light: 4, entityId: 'input_boolean.master_bedroom_suite_door_1' },
+ { room: 'l_master_bedroom_suite', light: 5, entityId: 'input_boolean.master_bedroom_suite_dresser_1' },
+ { room: 'l_master_bedroom_bathroom', light: 1, entityId: 'input_boolean.master_bedroom_bathroom_spots_1' },
+ { room: 'l_master_bedroom_bathroom', light: 2, entityId: 'input_boolean.master_bedroom_bathroom_fan_1' },
+ { room: 'l_exterior_veranda', light: 1, entityId: 'input_boolean.exterior_veranda_main_1' },
+ { room: 'l_exterior_yard', light: 1, entityId: 'input_boolean.exterior_yard_bbq_1' },
+ { room: 'l_exterior_entrance', light: 1, entityId: 'input_boolean.exterior_entrance_door_1' },
+ { room: 'l_exterior_yard', light: 2, entityId: 'input_boolean.exterior_yard_front_lights_1' },
+ { room: 'l_exterior_yard', light: 3, entityId: 'input_boolean.exterior_yard_backyard_flood_light_1' },
+ { room: 'l_exterior_yard', light: 4, entityId: 'input_boolean.exterior_yard_backyard_flood_light_2' },
+ { room: 'l_exterior_yard', light: 5, entityId: 'input_boolean.exterior_yard_master_bedroom_flood_light_1' },
+ { room: 'l_exterior_yard', light: 6, entityId: 'input_boolean.exterior_yard_driveway_1' },
],
// HA entity_id substring (after domain.) without trailing _N → NVL room key. For HA to NVL.
entityToRoom: {
- // living_room: 'livingRoom',
- living_room_new: 'cmd_livingroom',
- // kitchen: 'kitchen',
- // master_bedroom: 'masterBedroom',
- // bathroom: 'bathroom',
- // bedroom_1: 'bedroom_1',
- // bedroom_2: 'bedroom_2',
- // dinning_room: 'dinningRoom',
- // entrance: 'entrance',
- // hallway: 'hallway',
- // pantry: 'pantry',
- // guest_wc: 'guestWc',
- // out_veranda: 'outVeranda',
- // out_front: 'outFront',
- // out_back: 'outBack',
- // out_side: 'outSide',
+ open_plan_living_room_main: 'open_plan_living_room',
+ open_plan_living_room_spots: 'open_plan_living_room',
+ open_plan_living_room_strip: 'open_plan_living_room',
+ open_plan_dining_room_main: 'open_plan_dining_room',
+ open_plan_dining_room_spots: 'open_plan_dining_room',
+ open_plan_entrance_mirror: 'open_plan_entrance',
+ open_plan_entrance_main: 'open_plan_entrance',
+ open_plan_guest_wc_main: 'open_plan_guest_wc',
+ open_plan_guest_wc_strip: 'open_plan_guest_wc',
+ open_plan_guest_wc_fan: 'open_plan_guest_wc',
+ kitchen_kitchen_main: 'kitchen_kitchen',
+ kitchen_kitchen_island: 'kitchen_kitchen',
+ kitchen_kitchen_cabinets: 'kitchen_kitchen',
+ kitchen_kitchen_fridge: 'kitchen_kitchen',
+ kitchen_pantry_main: 'kitchen_pantry',
+ bedrooms_office_main: 'bedrooms_office',
+ bedrooms_office_strip: 'bedrooms_office',
+ bedrooms_hallway_main: 'bedrooms_hallway',
+ bedrooms_laundry_main: 'bedrooms_laundry',
+ bedrooms_shower_spots: 'bedrooms_shower',
+ bedrooms_shower_fan: 'bedrooms_shower',
+ bedrooms_bedroom_main: 'bedrooms_bedroom',
+ bedrooms_bedroom_spots: 'bedrooms_bedroom',
+ master_bedroom_suite_main: 'master_bedroom_suite',
+ master_bedroom_suite_spots: 'master_bedroom_suite',
+ master_bedroom_suite_strip: 'master_bedroom_suite',
+ master_bedroom_suite_door: 'master_bedroom_suite',
+ master_bedroom_suite_dresser: 'master_bedroom_suite',
+ master_bedroom_bathroom_spots: 'master_bedroom_bathroom',
+ master_bedroom_bathroom_fan: 'master_bedroom_bathroom',
+ exterior_veranda_main: 'exterior_veranda',
+ exterior_yard_bbq: 'exterior_yard',
+ exterior_entrance_door: 'exterior_entrance',
+ exterior_yard_front_lights: 'exterior_yard',
+ exterior_yard_backyard_flood_light: 'exterior_yard',
+ exterior_yard_master_bedroom_flood_light: 'exterior_yard',
+ exterior_yard_driveway: 'exterior_yard',
},
// // Zigbee: friendly_name → single room (fallback when switchBindings missing).
// deviceToRoom: {
- // 'Office Switch': 'cmd_livingroom',
+ // 'Office Switch': 'open_plan_living_room',
// },
// When using a multi-device Zigbee node, payload is keyed by IEEE (e.g. 0xa4c138a5b9771b05). Map IEEE → friendly name so switchBindings by name still works.
@@ -68,7 +123,7 @@ const ROOM_CONFIG = {
// Optional: bind by IEEE instead of friendly name (same shape as switchBindings). Use if the node never sends friendly_name.
// switchBindingsByDeviceId: {
- // '0xa4c138a5b9771b05': { 1: { room: 'cmd_livingroom', light: 1 }, 2: { room: 'cmd_out', light: 2 } },
+ // '0xa4c138a5b9771b05': { 1: { room: 'open_plan_living_room', light: 1 }, 2: { room: 'open_plan_dining_room', light: 1 } },
// },
/**
@@ -78,8 +133,8 @@ const ROOM_CONFIG = {
*/
switchBindings: {
'Office Switch': {
- 1: { room: 'cmd_livingroom', light: 1 }, // button 1 → light 1
- 2: { room: 'cmd_livingroom', light: 2 }, // button 2 → light 1 (same light; other switches could do button 2 → light 2)
+ 1: { room: 'open_plan_living_room', light: 1 },
+ 2: { room: 'open_plan_dining_room', light: 1 },
},
},
};
diff --git a/node-red/room-config-loader.js b/node-red/room-config-loader.js
index 9fd17d2..0b633dd 100644
--- a/node-red/room-config-loader.js
+++ b/node-red/room-config-loader.js
@@ -4,17 +4,34 @@
// 3. Replace the ROOM_CONFIG below with your paste. Keep the two lines at the end.
const ROOM_CONFIG = {
- roomNames: ['cmd_livingroom', 'cmd_out'],
- lightEntityMap: [
- { room: 'light_livingRoom', light: 1, entityId: 'input_boolean.living_room_new' },
+ roomNames: [
+ 'open_plan_living_room',
+ 'open_plan_dining_room',
+ 'open_plan_entrance',
+ 'open_plan_guest_wc',
+ 'kitchen_kitchen',
+ 'kitchen_pantry',
+ 'bedrooms_office',
+ 'bedrooms_hallway',
+ 'bedrooms_laundry',
+ 'bedrooms_shower',
+ 'bedrooms_bedroom',
+ 'master_bedroom_suite',
+ 'master_bedroom_bathroom',
+ 'exterior_veranda',
+ 'exterior_entrance',
+ 'exterior_yard',
],
- entityToRoom: { living_room_new: 'cmd_livingroom' },
- deviceToRoom: { 'Office Switch': 'cmd_livingroom' },
+ lightEntityMap: [
+ { room: 'l_open_plan_living_room', light: 1, entityId: 'input_boolean.open_plan_living_room_main_1' },
+ ],
+ entityToRoom: { open_plan_living_room_main: 'open_plan_living_room' },
+ deviceToRoom: { 'Office Switch': 'open_plan_living_room' },
deviceIdToName: {},
switchBindings: {
'Office Switch': {
- 1: { room: 'cmd_livingroom', light: 1 },
- 2: { room: 'cmd_livingroom', light: 1 },
+ 1: { room: 'open_plan_living_room', light: 1 },
+ 2: { room: 'open_plan_dining_room', light: 1 },
},
},
};