node-red-contrib-dmx-for-ha 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,35 @@
1
+ # Subflow Reference Implementation
2
+
3
+ These files are the original Node-RED subflow implementation of the nodes.
4
+ They predate the packaged node version and are kept here as a reference.
5
+
6
+ ## What these are
7
+
8
+ Before the nodes were packaged as proper NR nodes (`ha-mqtt-*`), they ran
9
+ as subflows — Node-RED function nodes with env var menus. The `.js` files
10
+ here are the function node code that was pasted inside each subflow.
11
+
12
+ ## When to use these
13
+
14
+ - As a reference for the business logic if you need to understand what the
15
+ packaged node `.js` files are doing
16
+ - For debugging — the subflow version can be imported into NR without npm install
17
+ - As a fallback if the packaged nodes are not available in your NR version
18
+
19
+ ## Import instructions
20
+
21
+ 1. Import `subflow_definitions.json` into NR — do NOT deploy yet
22
+ 2. Wire up instances and deploy
23
+ 3. Paste the relevant `.js` file contents into each function node
24
+
25
+ See `/docs/nr_subflow_gotchas.md` for important NR import notes.
26
+
27
+ ## Versions
28
+
29
+ | File | Version |
30
+ |---|---|
31
+ | dmx_node_v0.5.9.js | 0.5.9 |
32
+ | dmx_group_node_v0.3.8.js | 0.3.8 |
33
+ | relay_node_v4.0.2.js | 4.0.2 |
34
+ | button_node_v5.0.3.js | 5.0.3 |
35
+ | pir_node_v1.0.3.js | 1.0.3 |
@@ -0,0 +1,324 @@
1
+ // BUTTON NODE — HOME ASSISTANT in NODE-RED
2
+ // Discord: @deswaggy | Version: 5.0.3
3
+ // Extensions: "BETTER COMMENTS", "TODO TREE"
4
+
5
+ //! BIG & BRIGHT
6
+ //@ Topic Headers
7
+ //# Description / Explanations
8
+ //$ Logic
9
+ //^ Debug Comments
10
+ //? Breaks / Headers
11
+
12
+ // Naming conventions:
13
+ // <env_var> = Sub-flow environmental (Edit subflow template)
14
+ // <<<flow_env>>> = Flow-level environmental (Tab → gear → flow envs)
15
+
16
+ // Role of this node:
17
+ // Receives physical wall button press payloads from the button controller
18
+ // via MQTT, filters for its own payload, and reports the press to HA as
19
+ // a binary_sensor state change (ON → auto-clears via HA off_delay).
20
+ //
21
+ // Also discovers a companion HA "button" entity so the physical wall button
22
+ // can be mirrored in the HA dashboard. Pressing the UI button follows the
23
+ // same code path as a physical press.
24
+ //
25
+ // Physical press path:
26
+ // Controller MQTT → NR MQTT In → Button Node → binary_sensor ON → HA automation
27
+ //
28
+ // UI mirror press path:
29
+ // HA UI button → HA publishes to cmd_t → NR MQTT In → Button Node → same path
30
+ //
31
+ // NO websocket dependency. Pure MQTT throughout.
32
+ //
33
+ // Controller payload format: "{panelId}-{GPIOpin}" e.g. "10-54"
34
+ // Panel IDs: 10=Master B.Ctrl#1, 11=#2, 12=#3, 13=PIR#1, 14=PIR#2 etc.
35
+
36
+ //! IMPORTANT — NODE-RED FUNCTION NODE SETUP:
37
+ // This node requires EXACTLY 4 outputs configured in the NR function node settings.
38
+ // Output 1: HA MQTT (binary_sensor state + discovery config) → wire to MQTT Out node
39
+ // Output 2: MQTT subscribe / unsubscribe actions → wire to MQTT In node
40
+ // Output 3: Node status + debug → wire to Status / Debug node
41
+
42
+ // ─────────────────────────────────────────────────────────────────────────────
43
+ //!@ CONFIG — all env reads in one place
44
+ // ─────────────────────────────────────────────────────────────────────────────
45
+
46
+ const CFG = {
47
+ //@ HA identity
48
+ discoveryPrefix: env.get("<HA_discovery_prefix>"),
49
+ siteId: env.get("<HA_site_unique_id>"),
50
+ //^ binary_sensor is always the automation trigger entity — hardcoded, not env-driven
51
+ component: "binary_sensor",
52
+ uidPrefix: env.get("<HA_unique_id_prefix>"), //^ S = Switch (electrician convention)
53
+ uid: env.get("<HA_unique_id>"), //^ cable number from electrical plan
54
+ uidPostfix: String(env.get("<HA_unique_id_postfix>") || ""), //^ button letter A/B/C/D
55
+
56
+ //@ MQTT — HA side
57
+ retain: env.get("<HA_MQTT_retain_flag>"),
58
+ qos: env.get("<HA_MQTT_QOS>"),
59
+ configTopic: env.get("<HA_config_topic>"),
60
+ commandTopic: env.get("<HA_command_topic>"),
61
+ stateTopic: env.get("<HA_state_topic>"),
62
+
63
+ //@ HA features
64
+ enabledByDefault: env.get("<HA_enabled_by_default>"),
65
+ icon: env.get("<HA_icon>"),
66
+
67
+ //@ Button specific
68
+ //^ buttonPayload: the controller payload that identifies THIS button
69
+ //^ Format: "{panelId}-{GPIOpin}" e.g. "10-54" = controller 10, GPIO pin 54
70
+ buttonPayload: String(env.get("<BUTTON_Payload>")),
71
+ buttonPosition: env.get("<BUTTON_Position>"), //^ Top Left, Top Right, Bottom Left, Bottom Right etc.
72
+ buttonLedColor: env.get("<BUTTON_Info_LED_Color>"), //^ Blue, Red, Green, White, Orange
73
+ //^ How long HA shows the button as ON before auto-clearing (seconds, float).
74
+ //^ Implemented via HA binary_sensor off_delay — HA manages the timer, not NR.
75
+ //^ Default 0.5s feels responsive without being too brief for automation edge detection.
76
+ holdTimeSecs: parseFloat(env.get("<BUTTON_hold_time_seconds>")) || 0.5,
77
+
78
+ //@ Controller — physical button hardware side
79
+ controllerZone: env.get("<CONTROLLER_zone>"),
80
+ controllerSubscribeTopic: env.get("<CONTROLLER_MQTT_subscribe_topic>"),
81
+ //^ Full topic path. New firmware: "MW3D/Master/10/buttons"
82
+ //^ Old firmware (translation layer): "buttons"
83
+ controllerFw: env.get("<CONTROLLER_fw_v>"),
84
+
85
+ //@ Button type — default "Wall", kept for future controller/type flexibility
86
+ buttonType: env.get("<BUTTON_Type>") || "Wall",
87
+
88
+ //@ Controller model — for hw_version documentation and future multi-controller support
89
+ controllerModel: env.get("<CONTROLLER_Model>") || "SuperHouse_ButtonController",
90
+
91
+ //@ Device metadata
92
+ device: {
93
+ situation: env.get("<DEVICE_situation>"),
94
+ area: env.get("<DEVICE_area>"),
95
+ subLocation: env.get("<DEVICE_Sub_Location>"),
96
+ },
97
+
98
+ //@ Node metadata
99
+ nodeCode: env.get("<NODE_code>") || "BUTTON NODE",
100
+ nodeSw: env.get("<NODE_sw_v>"),
101
+ };
102
+
103
+ // ─────────────────────────────────────────────────────────────────────────────
104
+ //@ TOPIC BUILDERS
105
+ // ─────────────────────────────────────────────────────────────────────────────
106
+
107
+ /** HA MQTT base topic for this button's binary_sensor entity. */
108
+ const fixtureTopic = `${CFG.discoveryPrefix}/${CFG.component}/${CFG.uidPrefix}-${CFG.uid}${CFG.uidPostfix}`;
109
+
110
+ /** HA MQTT base topic for the companion UI button entity (mirror of physical button). */
111
+ const uiButtonTopic = `${CFG.discoveryPrefix}/button/${CFG.uidPrefix}-${CFG.uid}${CFG.uidPostfix}-BTN`;
112
+
113
+ /** The command topic HA publishes to when the UI button is pressed. */
114
+ const uiButtonCmdTopic = `${uiButtonTopic}/${CFG.commandTopic}`;
115
+
116
+ /** Short fixture ID for NR node status messages. */
117
+ const fixtureId = `${CFG.uidPrefix}-${CFG.uid}${CFG.uidPostfix}`;
118
+
119
+ // ─────────────────────────────────────────────────────────────────────────────
120
+ //@ MQTT SEND HELPERS
121
+ // ─────────────────────────────────────────────────────────────────────────────
122
+
123
+ /**
124
+ * Publish binary_sensor state to HA on output 1.
125
+ * @param {"ON"|"OFF"} state
126
+ */
127
+ function sendState(state) {
128
+ node.send([{
129
+ retain: CFG.retain,
130
+ qos: CFG.qos,
131
+ topic: `${fixtureTopic}/${CFG.stateTopic}`,
132
+ payload: state,
133
+ }, null, null, null]);
134
+ }
135
+
136
+ /** Send Node-RED status + debug message on output 3. */
137
+ function sendStatus(fill, shape, text) {
138
+ node.send([null, null, {
139
+ debug: { node: fixtureId, message: text },
140
+ status: { fill, shape, text },
141
+ }, null, null]);
142
+ }
143
+
144
+ // ─────────────────────────────────────────────────────────────────────────────
145
+ //@ SERIAL NUMBER BUILDER
146
+ //# SHARED PATTERN — extract to shared utility on node package build
147
+ // ─────────────────────────────────────────────────────────────────────────────
148
+
149
+ function buildSerialNumber() {
150
+ //# Human-readable format matching DMX Node convention.
151
+ //# Cable ID and payload are what a field tech needs — not a machine slug.
152
+ return `(${CFG.uidPrefix}-${CFG.uid}${CFG.uidPostfix}) Payload: ${CFG.buttonPayload} — See ⓘ MQTT Info below.`;
153
+ }
154
+
155
+ /** Build a clean slug for the HA object_id (entity_id prefix). */
156
+ function buildObjectId() {
157
+ //# object_id sets the HA entity_id: binary_sensor.s_33a
158
+ //# Matches plan ID directly — what electricians and techs expect.
159
+ return `${CFG.uidPrefix}_${CFG.uid}${CFG.uidPostfix}`.toLowerCase().replace(/[^a-z0-9_]/g, '_');
160
+ }
161
+
162
+ // ─────────────────────────────────────────────────────────────────────────────
163
+ //@ PRESS HANDLER
164
+ //# Used for both physical button press and HA UI button press — identical path.
165
+ //# HA auto-clears the binary_sensor via off_delay in the discovery payload.
166
+ //# NR does not need a timer — HA manages the hold duration.
167
+ // ─────────────────────────────────────────────────────────────────────────────
168
+
169
+ function handlePress(source) {
170
+ console.log(`${fixtureId} press — source:${source} position:${CFG.buttonPosition}`);
171
+ sendState("ON");
172
+ sendStatus("blue", "dot", `${fixtureId} pressed — ${CFG.buttonPosition}`);
173
+ }
174
+
175
+ // ─────────────────────────────────────────────────────────────────────────────
176
+ //@ DEVICE REQUEST HANDLERS
177
+ // ─────────────────────────────────────────────────────────────────────────────
178
+
179
+ function buildSerialNumberStr() {
180
+ return buildSerialNumber();
181
+ }
182
+
183
+ function handleDeviceAdd() {
184
+ const { device: dev } = CFG;
185
+
186
+ //# Binary sensor discovery — the automation trigger entity
187
+ const binarySensorDiscovery = {
188
+ retain: CFG.retain,
189
+ qos: CFG.qos,
190
+ topic: `${fixtureTopic}/${CFG.configTopic}`,
191
+ payload: {
192
+ unique_id: `${CFG.uidPrefix}-${CFG.uid}${CFG.uidPostfix}`,
193
+ //^ object_id sets the HA entity_id: binary_sensor.s_33a
194
+ //^ Locked to plan ID — survives user renaming the friendly name in HA UI.
195
+ object_id: buildObjectId(),
196
+ name: `${CFG.buttonPosition} button ${dev.situation} the ${CFG.controllerZone} ${dev.area} ${dev.subLocation}`,
197
+ stat_t: `${fixtureTopic}/${CFG.stateTopic}`,
198
+ //^ off_delay: HA automatically clears the binary_sensor after this many seconds.
199
+ //^ NR publishes ON, HA manages the timer and publishes OFF itself.
200
+ //^ This removes the need for any NR-side timer logic.
201
+ off_delay: CFG.holdTimeSecs,
202
+ enabled_by_default: CFG.enabledByDefault,
203
+ icon: CFG.icon,
204
+ device: {
205
+ identifiers: `${CFG.component}-${CFG.uidPrefix}-${CFG.uid}${CFG.uidPostfix}`,
206
+ name: `(${CFG.uidPrefix}-${CFG.uid}${CFG.uidPostfix}) - Wall Button/s ${dev.situation} the ${CFG.controllerZone} - ${dev.area} - ${dev.subLocation}`,
207
+ model: `Wall button/s located ${dev.situation} the ${CFG.controllerZone}-${dev.area} - ${dev.subLocation}`,
208
+ model_id: `referenced on plan as: (${CFG.uidPrefix}-${CFG.uid}${CFG.uidPostfix}`,
209
+ suggested_area: `${CFG.controllerZone} ${dev.area} ${dev.subLocation}`,
210
+ hw_version: `${CFG.buttonType} button — ${CFG.buttonLedColor} LED indicator, Green Cat5 cable. ` +
211
+ `Controller model: ${CFG.controllerModel} in ${CFG.controllerZone} Server Rack, firmware: ${CFG.controllerFw}. ` +
212
+ `Publishes payload "${CFG.buttonPayload}" on topic: ${CFG.controllerSubscribeTopic}`,
213
+ serial_number: buildSerialNumberStr(),
214
+ sw_version: `${CFG.nodeCode}: ${CFG.nodeSw}`,
215
+ manufacturer: "DeSwaggy — Discord: @deswaggy",
216
+ },
217
+ },
218
+ };
219
+
220
+ //# UI button discovery — the dashboard mirror entity
221
+ //# Pressing this in HA UI publishes "PRESS" to uiButtonCmdTopic.
222
+ //# NR receives it and follows the same path as a physical press.
223
+ //# Stateless in HA — no state topic. Just a command trigger.
224
+ const uiButtonDiscovery = {
225
+ retain: CFG.retain,
226
+ qos: CFG.qos,
227
+ topic: `${uiButtonTopic}/${CFG.configTopic}`,
228
+ payload: {
229
+ unique_id: `${CFG.uidPrefix}-${CFG.uid}${CFG.uidPostfix}-BTN`,
230
+ object_id: `${buildObjectId()}_btn`,
231
+ name: `${CFG.buttonPosition} (UI) ${dev.situation} the ${CFG.controllerZone} ${dev.area} ${dev.subLocation}`,
232
+ cmd_t: uiButtonCmdTopic,
233
+ payload_press: "PRESS",
234
+ enabled_by_default: CFG.enabledByDefault,
235
+ icon: CFG.icon,
236
+ //# Same device as binary_sensor — one device, two entities in HA
237
+ device: {
238
+ identifiers: `${CFG.component}-${CFG.uidPrefix}-${CFG.uid}${CFG.uidPostfix}`,
239
+ },
240
+ },
241
+ };
242
+
243
+ //# Subscribe to controller topic (physical presses) AND UI button cmd topic
244
+ const subscribeController = {
245
+ retain: CFG.retain,
246
+ qos: CFG.qos,
247
+ topic: CFG.controllerSubscribeTopic,
248
+ action: "subscribe",
249
+ };
250
+ const subscribeUiButton = {
251
+ retain: CFG.retain,
252
+ qos: CFG.qos,
253
+ topic: uiButtonCmdTopic,
254
+ action: "subscribe",
255
+ };
256
+
257
+ //# Send binary_sensor discovery first, then UI button discovery
258
+ node.send([binarySensorDiscovery, subscribeController, { status: { fill: "green", shape: "ring", text: `${fixtureId} discovery sent — awaiting press` } }, null, null]);
259
+ node.send([uiButtonDiscovery, subscribeUiButton, null, null, null]);
260
+
261
+ console.log(`${fixtureId} device added — position:${CFG.buttonPosition} payload:${CFG.buttonPayload}`);
262
+ console.log(`${fixtureId} subscribed to: ${CFG.controllerSubscribeTopic} and ${uiButtonCmdTopic}`);
263
+ }
264
+
265
+ function handleDeviceRemove() {
266
+ node.send([
267
+ { retain: CFG.retain, qos: CFG.qos, topic: `${fixtureTopic}/${CFG.configTopic}`, payload: "" },
268
+ { retain: CFG.retain, qos: CFG.qos, topic: CFG.controllerSubscribeTopic, action: "unsubscribe" },
269
+ { status: { fill: "red", shape: "ring", text: `${fixtureId} removed` } },
270
+ null,
271
+ null,
272
+ ]);
273
+ //# Also remove UI button entity and unsubscribe from its cmd topic
274
+ node.send([
275
+ { retain: CFG.retain, qos: CFG.qos, topic: `${uiButtonTopic}/${CFG.configTopic}`, payload: "" },
276
+ { retain: CFG.retain, qos: CFG.qos, topic: uiButtonCmdTopic, action: "unsubscribe" },
277
+ null, null, null,
278
+ ]);
279
+ console.log(`${fixtureId} device removed.`);
280
+ }
281
+
282
+ // ─────────────────────────────────────────────────────────────────────────────
283
+ //!? ENTRY POINT
284
+ // ─────────────────────────────────────────────────────────────────────────────
285
+
286
+ node.send([null, null, null, msg]);
287
+
288
+ console.log(`${fixtureId} payload:"${msg.payload}" topic:"${msg.topic || ""}"`);
289
+
290
+ //? ── DEVICE REQUEST — add / remove / debug ───────────────────────────────────
291
+ if (msg.device != null && (typeof msg.device === "string" || msg.device?.request != null)) {
292
+ const _devReq = typeof msg.device === "string" ? msg.device : msg.device.request;
293
+ console.log(`${fixtureId} device.request: ${_devReq}`);
294
+ switch (_devReq) {
295
+ case "add": handleDeviceAdd(); break;
296
+ case "remove": handleDeviceRemove(); break;
297
+ case "debug": console.log(`${fixtureId} DEBUG:`, JSON.stringify(msg)); break;
298
+ default: node.warn(`${fixtureId} — unknown device.request: "${_devReq}"`);
299
+ }
300
+
301
+ //? ── UI BUTTON PRESS — from HA dashboard ─────────────────────────────────────
302
+ } else if (msg.topic === uiButtonCmdTopic && msg.payload === "PRESS") {
303
+ handlePress("HA UI");
304
+
305
+ //? ── PHYSICAL BUTTON PRESS — from controller MQTT ────────────────────────────
306
+ } else if (String(msg.payload) === CFG.buttonPayload) {
307
+ handlePress("physical");
308
+
309
+ //? ── UNKNOWN MESSAGE ─────────────────────────────────────────────────────────
310
+ } else {
311
+ //# Payload didn't match — this button node is ignoring a press meant for another button.
312
+ //# This is normal and expected on the shared controller topic. No warn needed.
313
+ //# Only warn if the topic is the UI command topic but payload is unexpected.
314
+ if (msg.topic === uiButtonCmdTopic) {
315
+ node.warn(
316
+ `${fixtureId} — unexpected payload on UI button cmd topic. ` +
317
+ `Expected "PRESS", got: "${msg.payload}"`
318
+ );
319
+ }
320
+ //# Silently ignore all other payloads — they belong to sibling button nodes.
321
+ }
322
+
323
+ node.done();
324
+ return [null, null, null, msg];