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,326 @@
1
+ <!-- ============================================================
2
+ ha-mqtt-button — Button Node Editor
3
+ Package: node-red-contrib-dmx-for-ha
4
+ Author: DeSwaggy — Discord: @deswaggy
5
+ ============================================================ -->
6
+
7
+ <script type="text/javascript">
8
+
9
+ RED.nodes.registerType('ha-mqtt-button', {
10
+ category: 'DMX for HA',
11
+ color: '#27ae60',
12
+ icon: 'font-awesome/fa-hand-pointer-o',
13
+ inputs: 1,
14
+ outputs: 0,
15
+ inputLabels: ['Input'],
16
+ paletteLabel: 'Button',
17
+
18
+ defaults: {
19
+ name: { value: '' },
20
+ config: { value: '', type: 'ha-mqtt-config', required: true },
21
+ // Identity
22
+ uid: { value: '', required: true },
23
+ uidPostfix: { value: '' },
24
+ // Location
25
+ area: { value: 'Area 52' },
26
+ situation: { value: 'in' },
27
+ subLocation: { value: '' },
28
+ // Button specifics
29
+ buttonPosition: { value: '' },
30
+ buttonPayload: { value: '', required: true },
31
+ subscribeTopic: { value: 'buttons' },
32
+ // Options
33
+ haIcon: { value: 'mdi:gesture-tap-button' },
34
+ ledColor: { value: 'Blue' },
35
+ holdTime: { value: '0.5' },
36
+ },
37
+
38
+ label: function () {
39
+ if (this.name) return this.name;
40
+ const id = this.uid || '?';
41
+ const postfix = this.uidPostfix || '';
42
+ return 'S-' + id + postfix;
43
+ },
44
+
45
+ labelStyle: function () {
46
+ return this.name ? 'node_label_italic' : '';
47
+ },
48
+
49
+ oneditprepare: function () {
50
+ const node = this;
51
+ const areas = [
52
+ { v: 'Area 52', l: 'TBC' },
53
+ { v: 'Attic Roof', l: 'Attic Roof' },
54
+ { v: 'Backyard', l: 'Backyard' },
55
+ { v: 'Balcony', l: 'Balcony' },
56
+ { v: 'Bathroom', l: 'Bathroom' },
57
+ { v: 'Bedroom 1', l: 'Bedroom 1' },
58
+ { v: 'Bedroom 2', l: 'Bedroom 2' },
59
+ { v: 'Bedroom 3', l: 'Bedroom 3' },
60
+ { v: 'Bedroom 4', l: 'Bedroom 4' },
61
+ { v: 'Carport', l: 'Carport' },
62
+ { v: 'Cellar', l: 'Cellar' },
63
+ { v: 'Dining', l: 'Dining' },
64
+ { v: 'Driveway', l: 'Driveway' },
65
+ { v: 'Entry', l: 'Entry' },
66
+ { v: 'Garage', l: 'Garage' },
67
+ { v: 'Gym', l: 'Gym' },
68
+ { v: 'Hallway', l: 'Hallway' },
69
+ { v: 'Kitchen', l: 'Kitchen' },
70
+ { v: 'Laundry', l: 'Laundry' },
71
+ { v: 'Link Bridge (Upper)', l: 'Link Bridge Upper' },
72
+ { v: 'Link Bridge (Lower)', l: 'Link Bridge Lower' },
73
+ { v: 'Living', l: 'Living' },
74
+ { v: 'Media', l: 'Media' },
75
+ { v: 'Office', l: 'Office' },
76
+ { v: 'Pantry', l: 'Pantry' },
77
+ { v: 'Pass Over', l: 'Pass Over' },
78
+ { v: 'Pass Under', l: 'Pass Under' },
79
+ { v: 'Passageway', l: 'Passageway' },
80
+ { v: 'Pool', l: 'Pool' },
81
+ { v: 'Portico', l: 'Portico' },
82
+ { v: 'PowderRoom', l: 'PowderRoom' },
83
+ { v: 'Reading', l: 'Reading' },
84
+ { v: 'Rumpus', l: 'Rumpus' },
85
+ { v: 'Spa', l: 'Spa' },
86
+ { v: 'Stairs', l: 'Stairs' },
87
+ { v: 'Store', l: 'Store' },
88
+ { v: 'SubFloor', l: 'SubFloor' },
89
+ { v: 'Terrace', l: 'Terrace' },
90
+ ];
91
+ const subAreas = [
92
+ { v: '', l: 'None' },
93
+ { v: 'Balcony', l: 'Balcony' },
94
+ { v: 'Bed (North)', l: 'Bed North' },
95
+ { v: 'Bed (South)', l: 'Bed South' },
96
+ { v: 'Bed (East)', l: 'Bed East' },
97
+ { v: 'Bed (West)', l: 'Bed West' },
98
+ { v: 'Ceiling', l: 'Ceiling' },
99
+ { v: 'Dress', l: 'Dress' },
100
+ { v: 'Ensuite', l: 'Ensuite' },
101
+ { v: 'Exterior', l: 'Exterior' },
102
+ { v: 'Ground Floor', l: 'Ground Floor' },
103
+ { v: '1st Floor', l: '1st Floor' },
104
+ { v: 'Landing', l: 'Landing' },
105
+ { v: 'Office', l: 'Office' },
106
+ { v: 'Stairs', l: 'Stairs' },
107
+ { v: 'WIR', l: 'WIR' },
108
+ { v: '(North)', l: 'North' },
109
+ { v: '(South)', l: 'South' },
110
+ { v: '(East)', l: 'East' },
111
+ { v: '(West)', l: 'West' },
112
+ { v: 'Void', l: 'Void' },
113
+ ];
114
+
115
+ const areaEl = $('#node-input-area');
116
+ areaEl.empty();
117
+ areas.forEach(function (a) {
118
+ areaEl.append($('<option>').val(a.v).text(a.l)
119
+ .prop('selected', a.v === node.area));
120
+ });
121
+
122
+ const subEl = $('#node-input-subLocation');
123
+ subEl.empty();
124
+ subAreas.forEach(function (s) {
125
+ subEl.append($('<option>').val(s.v).text(s.l)
126
+ .prop('selected', s.v === node.subLocation));
127
+ });
128
+ },
129
+ });
130
+
131
+ </script>
132
+
133
+ <script type="text/html" data-template-name="ha-mqtt-button">
134
+
135
+ <div class="form-row">
136
+ <label for="node-input-name">
137
+ <i class="fa fa-tag"></i> Name
138
+ </label>
139
+ <input type="text" id="node-input-name"
140
+ placeholder="Optional — defaults to S-10-A on canvas" />
141
+ </div>
142
+
143
+ <div class="form-row">
144
+ <label for="node-input-config">
145
+ <i class="fa fa-cog"></i> Config
146
+ </label>
147
+ <input type="text" id="node-input-config" />
148
+ </div>
149
+
150
+ <hr/>
151
+
152
+ <!-- ── BUTTON (* Required) ───────────────────────────────── -->
153
+ <div class="form-row">
154
+ <label style="width:100%; font-weight:bold; color:#999; font-size:0.85em; text-transform:uppercase; letter-spacing:0.05em;">
155
+ <i class="fa fa-hand-pointer-o"></i> Button &nbsp;<span style="color:#e74c3c">* Required</span>
156
+ </label>
157
+ </div>
158
+
159
+ <div class="form-row">
160
+ <label for="node-input-uid">
161
+ <i class="fa fa-tv"></i> Cable ID
162
+ </label>
163
+ <span style="margin-right:4px; font-weight:bold;">S -</span>
164
+ <input type="text" id="node-input-uid"
165
+ placeholder="10" style="width:70px" />
166
+ &nbsp;
167
+ <select id="node-input-uidPostfix" style="width:65px">
168
+ <option value="">(none)</option>
169
+ <option value="-A">-A</option>
170
+ <option value="-B">-B</option>
171
+ <option value="-C">-C</option>
172
+ <option value="-D">-D</option>
173
+ </select>
174
+ <span style="margin-left:8px; color:#999; font-size:0.85em;">
175
+ Plan ID — Button letter
176
+ </span>
177
+ </div>
178
+
179
+ <div class="form-row">
180
+ <label for="node-input-area">
181
+ <i class="fa fa-map-marker"></i> Area
182
+ </label>
183
+ <select id="node-input-area" style="width:55%"></select>
184
+ </div>
185
+
186
+ <div class="form-row">
187
+ <label for="node-input-situation">
188
+ <i class="fa fa-compass"></i> Situation
189
+ </label>
190
+ <select id="node-input-situation" style="width:55%">
191
+ <option value="in">in</option>
192
+ <option value="above">above</option>
193
+ <option value="below">below</option>
194
+ <option value="outside">outside</option>
195
+ <option value="throughout">throughout</option>
196
+ <option value="under">under</option>
197
+ <option value="over">over</option>
198
+ </select>
199
+ </div>
200
+
201
+ <div class="form-row">
202
+ <label for="node-input-subLocation">
203
+ <i class="fa fa-map-marker"></i> Sub-Area
204
+ </label>
205
+ <select id="node-input-subLocation" style="width:55%"></select>
206
+ </div>
207
+
208
+ <div class="form-row">
209
+ <label for="node-input-buttonPosition">
210
+ <i class="fa fa-hand-pointer-o"></i> Position
211
+ </label>
212
+ <select id="node-input-buttonPosition" style="width:55%">
213
+ <option value="Top Left">Top Left</option>
214
+ <option value="Top Right">Top Right</option>
215
+ <option value="Bottom Left">Bottom Left</option>
216
+ <option value="Bottom Right">Bottom Right</option>
217
+ <option value="Top">Top</option>
218
+ <option value="Bottom">Bottom</option>
219
+ <option value="Left">Left</option>
220
+ <option value="Right">Right</option>
221
+ <option value="Centre">Centre</option>
222
+ <option value="Single">Single</option>
223
+ </select>
224
+ </div>
225
+
226
+ <hr/>
227
+
228
+ <!-- ── CONTROLLER ────────────────────────────────────────── -->
229
+ <div class="form-row">
230
+ <label style="width:100%; font-weight:bold; color:#999; font-size:0.85em; text-transform:uppercase; letter-spacing:0.05em;">
231
+ <i class="fa fa-exchange"></i> Controller
232
+ </label>
233
+ </div>
234
+
235
+ <div class="form-row">
236
+ <label for="node-input-buttonPayload">
237
+ <i class="fa fa-hand-pointer-o"></i> Payload
238
+ </label>
239
+ <input type="text" id="node-input-buttonPayload"
240
+ placeholder="e.g. 10-54" style="width:120px" />
241
+ <span style="margin-left:8px; color:#999; font-size:0.85em;">
242
+ panelId-GPIOpin
243
+ </span>
244
+ </div>
245
+
246
+ <div class="form-row">
247
+ <label for="node-input-subscribeTopic">
248
+ <i class="fa fa-exchange"></i> Subscribe topic
249
+ </label>
250
+ <input type="text" id="node-input-subscribeTopic"
251
+ placeholder="buttons" style="width:55%" />
252
+ </div>
253
+
254
+ <hr/>
255
+
256
+ <!-- ── OPTIONS ───────────────────────────────────────────── -->
257
+ <div class="form-row">
258
+ <label style="width:100%; font-weight:bold; color:#999; font-size:0.85em; text-transform:uppercase; letter-spacing:0.05em;">
259
+ <i class="fa fa-sliders"></i> Options
260
+ </label>
261
+ </div>
262
+
263
+ <div class="form-row">
264
+ <label for="node-input-haIcon">
265
+ <i class="fa fa-image"></i> HA Icon
266
+ </label>
267
+ <input type="text" id="node-input-haIcon"
268
+ placeholder="mdi:gesture-tap-button" style="width:55%" />
269
+ </div>
270
+
271
+ <div class="form-row">
272
+ <label for="node-input-ledColor">
273
+ <i class="fa fa-circle"></i> LED Colour
274
+ </label>
275
+ <select id="node-input-ledColor" style="width:120px">
276
+ <option value="Blue">Blue</option>
277
+ <option value="Red">Red</option>
278
+ <option value="Green">Green</option>
279
+ <option value="White">White</option>
280
+ <option value="Orange">Orange</option>
281
+ <option value="None">None</option>
282
+ </select>
283
+ </div>
284
+
285
+ <div class="form-row">
286
+ <label for="node-input-holdTime">
287
+ <i class="fa fa-clock-o"></i> Hold time (s)
288
+ </label>
289
+ <input type="number" id="node-input-holdTime"
290
+ min="0.1" step="0.1" style="width:80px" />
291
+ <span style="margin-left:8px; color:#999; font-size:0.85em;">
292
+ HA auto-clears binary_sensor after this delay
293
+ </span>
294
+ </div>
295
+
296
+ </script>
297
+
298
+ <script type="text/html" data-help-name="ha-mqtt-button">
299
+ <p>
300
+ Wall button receiver. Listens for hardware controller MQTT payloads
301
+ and publishes to HA as a <code>binary_sensor</code> (for automations)
302
+ and a <code>button</code> entity (dashboard UI mirror).
303
+ </p>
304
+
305
+ <h3>Setup</h3>
306
+ <ol>
307
+ <li>Select your <strong>Config</strong> node</li>
308
+ <li>Set <strong>Cable ID</strong> and button letter — matches electrical plan</li>
309
+ <li>Set <strong>Payload</strong> — the <code>{panelId}-{GPIOpin}</code> string your controller publishes</li>
310
+ <li>Set <strong>Subscribe topic</strong> — the MQTT topic your controller publishes to</li>
311
+ <li>Deploy — two HA entities appear: a binary_sensor and a UI button mirror</li>
312
+ </ol>
313
+
314
+ <h3>Payload format</h3>
315
+ <p>
316
+ The controller publishes a plain string — e.g. <code>10-54</code>
317
+ meaning panel 10, GPIO pin 54. This node filters for its own
318
+ payload and silently ignores all others on the shared topic.
319
+ </p>
320
+
321
+ <h3>HA entities created</h3>
322
+ <ul>
323
+ <li><code>binary_sensor.s_10_a</code> — automation trigger, auto-clears via hold time</li>
324
+ <li><code>button.s_10_a_btn</code> — dashboard UI mirror, triggers same code path</li>
325
+ </ul>
326
+ </script>
@@ -0,0 +1,158 @@
1
+ // ============================================================
2
+ // ha-mqtt-button — Wall Button Node Runtime
3
+ // Package: node-red-contrib-dmx-for-ha
4
+ // Author: DeSwaggy — Discord: @deswaggy
5
+ // ============================================================
6
+
7
+ module.exports = function (RED) {
8
+
9
+ function HaMqttButtonNode(config) {
10
+ RED.nodes.createNode(this, config);
11
+ const node = this;
12
+
13
+ // ── Config & broker ───────────────────────────────────────
14
+ const cfg = RED.nodes.getNode(config.config);
15
+ if (!cfg) { node.error('Button: no config node selected'); return; }
16
+
17
+ const broker = RED.nodes.getNode(cfg.broker);
18
+ if (!broker) { node.error('Button: no MQTT broker in config'); return; }
19
+
20
+ broker.register(node);
21
+
22
+ // ── Node settings ─────────────────────────────────────────
23
+ const S = {
24
+ uid: config.uid || '',
25
+ uidPostfix: config.uidPostfix || '',
26
+ area: config.area || '',
27
+ situation: config.situation || 'in',
28
+ subLocation: config.subLocation || '',
29
+ buttonPosition: config.buttonPosition || 'Single',
30
+ buttonPayload: String(config.buttonPayload || ''),
31
+ subscribeTopic: config.subscribeTopic || 'buttons',
32
+ haIcon: config.haIcon || 'mdi:gesture-tap-button',
33
+ ledColor: config.ledColor || 'Blue',
34
+ holdTime: parseFloat(config.holdTime) || 0.5,
35
+ };
36
+
37
+ const fixtureId = `S-${S.uid}${S.uidPostfix}`;
38
+ const objectId = `s_${S.uid}${S.uidPostfix}`.toLowerCase().replace(/[^a-z0-9_]/g, '_');
39
+ const fixtureTopic = `${cfg.discoveryPrefix}/binary_sensor/${fixtureId}`;
40
+ const uiBtnTopic = `${cfg.discoveryPrefix}/button/${fixtureId}-BTN`;
41
+ const cfgTopic = `${fixtureTopic}/${cfg.configTopic}`;
42
+ const statTopic = `${fixtureTopic}/${cfg.stateTopic}`;
43
+ const uiBtnCfgTopic = `${uiBtnTopic}/${cfg.configTopic}`;
44
+ const uiBtnCmdTopic = `${uiBtnTopic}/${cfg.commandTopic}`;
45
+
46
+ // ── Helpers ───────────────────────────────────────────────
47
+ function pub(topic, payload, retain) {
48
+ broker.publish({
49
+ topic,
50
+ payload: typeof payload === 'object' ? JSON.stringify(payload) : String(payload),
51
+ qos: cfg.qos,
52
+ retain: retain !== undefined ? retain : cfg.retain,
53
+ });
54
+ }
55
+
56
+ function setStatus(fill, shape, text) {
57
+ node.status({ fill, shape, text });
58
+ }
59
+
60
+ // ── Press handler ─────────────────────────────────────────
61
+ function handlePress(source) {
62
+ pub(statTopic, 'ON', false);
63
+ setStatus('blue', 'dot', `${fixtureId} pressed — ${S.buttonPosition}`);
64
+ node.log(`${fixtureId} press — source:${source} position:${S.buttonPosition}`);
65
+ }
66
+
67
+ // ── Device add ────────────────────────────────────────────
68
+ function handleDeviceAdd() {
69
+ // Binary sensor discovery
70
+ const bsDiscovery = {
71
+ unique_id: fixtureId,
72
+ object_id: objectId,
73
+ name: `${S.buttonPosition} button ${S.situation} the ${cfg.zone} ${S.area} ${S.subLocation}`,
74
+ stat_t: statTopic,
75
+ off_delay: S.holdTime,
76
+ enabled_by_default: cfg.enabledDefault,
77
+ icon: S.haIcon,
78
+ device: {
79
+ identifiers: `binary_sensor-${fixtureId}`,
80
+ name: `(${fixtureId}) - Wall Button ${S.situation} the ${cfg.zone} - ${S.area} - ${S.subLocation}`,
81
+ model: `Wall button located ${S.situation} the ${cfg.zone} - ${S.area}`,
82
+ model_id: `referenced on plan as: (${fixtureId}`,
83
+ suggested_area: `${cfg.zone} ${S.area} ${S.subLocation}`,
84
+ hw_version: `Wall button — ${S.ledColor} LED. Publishes "${S.buttonPayload}" on topic: ${S.subscribeTopic}`,
85
+ serial_number: `(${fixtureId}) Payload: ${S.buttonPayload}`,
86
+ sw_version: 'ha-mqtt-button: 0.1.0',
87
+ manufacturer: 'DeSwaggy — Discord: @deswaggy',
88
+ },
89
+ };
90
+
91
+ // UI button discovery (dashboard mirror)
92
+ const uiDiscovery = {
93
+ unique_id: `${fixtureId}-BTN`,
94
+ object_id: `${objectId}_btn`,
95
+ name: `${S.buttonPosition} (UI) ${S.situation} the ${cfg.zone} ${S.area} ${S.subLocation}`,
96
+ cmd_t: uiBtnCmdTopic,
97
+ payload_press: 'PRESS',
98
+ enabled_by_default: cfg.enabledDefault,
99
+ icon: S.haIcon,
100
+ device: { identifiers: `binary_sensor-${fixtureId}` },
101
+ };
102
+
103
+ pub(cfgTopic, bsDiscovery, true);
104
+ pub(uiBtnCfgTopic, uiDiscovery, true);
105
+
106
+ // Subscribe to controller topic (physical presses)
107
+ broker.subscribe(S.subscribeTopic, cfg.qos, function (topic, payload) {
108
+ if (String(payload) === S.buttonPayload) handlePress('physical');
109
+ }, node.id);
110
+
111
+ // Subscribe to HA UI button cmd topic
112
+ broker.subscribe(uiBtnCmdTopic, cfg.qos, function (topic, payload) {
113
+ if (String(payload) === 'PRESS') handlePress('HA UI');
114
+ else node.warn(`${fixtureId} — unexpected UI payload: "${payload}"`);
115
+ }, node.id);
116
+
117
+ setStatus('green', 'ring', `${fixtureId} ready — awaiting press`);
118
+ node.log(`${fixtureId} device added — payload:${S.buttonPayload} topic:${S.subscribeTopic}`);
119
+ }
120
+
121
+ // ── Device remove ─────────────────────────────────────────
122
+ function handleDeviceRemove() {
123
+ pub(cfgTopic, '', true);
124
+ pub(uiBtnCfgTopic, '', true);
125
+ broker.unsubscribe(S.subscribeTopic, node.id);
126
+ broker.unsubscribe(uiBtnCmdTopic, node.id);
127
+ setStatus('red', 'ring', `${fixtureId} removed`);
128
+ node.log(`${fixtureId} device removed`);
129
+ }
130
+
131
+ // ── NR input entry point ──────────────────────────────────
132
+ node.on('input', function (msg, send, done) {
133
+ const devReq = typeof msg.device === 'string'
134
+ ? msg.device
135
+ : (msg.device && msg.device.request);
136
+
137
+ if (devReq) {
138
+ switch (devReq) {
139
+ case 'add': handleDeviceAdd(); break;
140
+ case 'remove': handleDeviceRemove(); break;
141
+ default: node.warn(`${fixtureId} — unknown device.request: "${devReq}"`);
142
+ }
143
+ } else {
144
+ node.warn(`${fixtureId} — unrecognised message received and dropped. See node documentation.`);
145
+ }
146
+ done();
147
+ });
148
+
149
+ // ── Cleanup ───────────────────────────────────────────────
150
+ node.on('close', function (done) {
151
+ broker.unsubscribe(S.subscribeTopic, node.id);
152
+ broker.unsubscribe(uiBtnCmdTopic, node.id);
153
+ broker.deregister(node, done);
154
+ });
155
+ }
156
+
157
+ RED.nodes.registerType('ha-mqtt-button', HaMqttButtonNode);
158
+ };