From 0af21f4dc388d2215d7b8c2f91569107dfe85617 Mon Sep 17 00:00:00 2001 From: nearxos Date: Wed, 1 Apr 2026 19:09:59 +0300 Subject: [PATCH] Refactor room and light configurations for Node-RED integration - Updated global variable lists in GVL_IO.gvl and GVL_NVL_placeholders.gvl to reflect new room naming conventions and structures. - Revised PLC_App.st to map new room configurations for lighting control. - Enhanced documentation in all-lights-and-rooms.md and ha-lights-and-rooms.md to align with updated room and light entity naming. - Adjusted room-config.js and related Node-RED flows to support the new configuration structure. This update improves the organization and clarity of room and light management within the Node-RED integration, ensuring consistency across the system. --- codesys/src/GVL/GVL_IO.gvl | 34 ++-- codesys/src/GVL/GVL_NVL_placeholders.gvl | 64 +++---- codesys/src/NVL/README.md | 16 +- codesys/src/NVL/nodered-payload.md | 8 +- codesys/src/POUs/PLC_App.st | 162 +++++++++--------- docs/Electrical/Ligts_Zones.csv | 42 +++++ docs/Electrical/lights_zones_canonical.csv | 40 +++++ docs/Electrical/lights_zones_cleaned.csv | 40 +++++ docs/Electrical/naming_rules.md | 70 ++++++++ docs/Electrical/validation_report.md | 37 ++++ docs/integration/all-lights-and-rooms.md | 23 ++- docs/integration/ha-lights-and-rooms.md | 32 ++-- .../lights_by_area_room_type_index.yaml | 47 +++++ node-red/config_files/flows.json | 14 +- node-red/config_files/room-config.js | 139 ++++++++++----- node-red/room-config-loader.js | 31 +++- 16 files changed, 579 insertions(+), 220 deletions(-) create mode 100644 docs/Electrical/Ligts_Zones.csv create mode 100644 docs/Electrical/lights_zones_canonical.csv create mode 100644 docs/Electrical/lights_zones_cleaned.csv create mode 100644 docs/Electrical/naming_rules.md create mode 100644 docs/Electrical/validation_report.md create mode 100644 docs/integration/lights_by_area_room_type_index.yaml 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 }, }, }, };