node-red-contrib-velbus-2026 0.9.0 → 0.9.1

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.
@@ -14,6 +14,45 @@ All feedback via GitHub issues, with examples and debug captures where possible.
14
14
 
15
15
  ---
16
16
 
17
+ ## v0.9.1 — 08/07/2026
18
+
19
+ ### Address validation bug — 12 of 19 node types, any address ≥100 decimal
20
+
21
+ - **Reported by Stuart:** adding a velbus-sensor node and selecting VMB4AN
22
+ from the scan dropdown showed a red "not configured correctly" triangle
23
+ no matter what was selected.
24
+ - **Root cause:** every node using the `address` field (as opposed to
25
+ `moduleAddr`, used by relay/dimmer nodes, which were unaffected) validated
26
+ it against `/^(0x)?[0-9a-fA-F]{1,2}$/` — a pattern that only ever made
27
+ sense if `address` were stored as a 1-2 character hex string. But
28
+ `oneditsave` on every one of these nodes stores it as a plain decimal
29
+ **number** instead (`parseInt(...)`). Since regex `.test()` coerces its
30
+ argument to a string first, any address whose decimal representation is
31
+ 1-2 digits (1-99) happened to accidentally still match — every digit
32
+ 0-9 is also a valid hex character — masking the bug for low addresses.
33
+ Any address **100 or higher** (`0x64`+) produces a 3-digit decimal string,
34
+ which cannot match a `{1,2}`-length pattern, so validation always failed.
35
+ 100-254 is a very ordinary real-world address range — this was a live,
36
+ fully user-facing bug, not an edge case.
37
+ - **Affected nodes (12):** velbus-blind, velbus-blind-s, velbus-blind-20,
38
+ velbus-button, velbus-energy, velbus-glass-panel, velbus-meteo,
39
+ velbus-pir, velbus-pir-20, velbus-sensor, velbus-sensor-20,
40
+ velbus-thermostat. Confirmed relay/dimmer/relay-20/dimmer-20 (which use
41
+ `moduleAddr`, no custom validate function) and velbus-clock (no fixed
42
+ address at all) were never affected.
43
+ - **Fix:** replaced the regex with a direct numeric range check
44
+ (`parseInt(v)` between 1 and 254 inclusive) — correct regardless of
45
+ whether the stored value is a number, a decimal string, or a legacy hex
46
+ string (`parseInt` auto-detects a `0x` prefix), so old saved flows keep
47
+ working unchanged.
48
+ - Verified: syntax-checked the extracted JS from all 12 files, and
49
+ confirmed the corrected logic against the actual failing range (50/80/99
50
+ → true both before and after; 100/150/254 → **false before, true after**;
51
+ 0/255/empty/non-numeric → false, correctly rejected as invalid addresses
52
+ both before and after).
53
+
54
+ ---
55
+
17
56
  ## v0.9.0 — 08/07/2026
18
57
 
19
58
  ### velbus-energy — new node (19th node), VMBPSUMNGR-20
package/HANDOVER.md ADDED
@@ -0,0 +1,906 @@
1
+ # HANDOVER.md — node-red-contrib-velbus-2026
2
+
3
+ **A complete technical reference for developing this Node-RED palette, written to assume
4
+ no prior context.** If you are picking this project up for the first time — whether
5
+ you're a new contributor, a new maintainer, or an AI assistant starting a fresh session
6
+ with no memory of previous work — this document should be sufficient on its own, together
7
+ with the source code in this repository, to continue development competently.
8
+
9
+ Current state at time of writing: **v0.9.0, 19 nodes, published on npm.**
10
+
11
+ ---
12
+
13
+ ## Table of contents
14
+
15
+ 1. [What this project is](#1-what-this-project-is)
16
+ 2. [Quick start](#2-quick-start)
17
+ 3. [Repository structure](#3-repository-structure)
18
+ 4. [Architecture overview](#4-architecture-overview)
19
+ 5. [Complete node reference](#5-complete-node-reference)
20
+ 6. [Complete module type registry](#6-complete-module-type-registry)
21
+ 7. [Critical protocol knowledge](#7-critical-protocol-knowledge)
22
+ 8. [Payload conventions](#8-payload-conventions)
23
+ 9. [Architecture decisions (settled — do not reopen casually)](#9-architecture-decisions-settled--do-not-reopen-casually)
24
+ 10. [Development environment setup](#10-development-environment-setup)
25
+ 11. [Testing without real hardware — the mock-harness pattern](#11-testing-without-real-hardware--the-mock-harness-pattern)
26
+ 12. [Testing status — what's actually verified](#12-testing-status--whats-actually-verified)
27
+ 13. [Known open issues](#13-known-open-issues)
28
+ 14. [Contribution and release workflow](#14-contribution-and-release-workflow)
29
+ 15. [Where to find protocol references](#15-where-to-find-protocol-references)
30
+ 16. [Code style rules](#16-code-style-rules)
31
+ 17. [License and attribution](#17-license-and-attribution)
32
+
33
+ ---
34
+
35
+ ## 1. What this project is
36
+
37
+ **Velbus** is a CAN-bus-based building automation system, manufactured by Velbus Belgium.
38
+ Installations consist of physical modules on a shared bus — relay outputs, dimmers,
39
+ touch-sensitive glass control panels, PIR motion sensors, blind/shutter controllers,
40
+ weather stations, and more — each with a unique bus address. Modules are commissioned
41
+ using Velbus's own **VelbusLink** software, and communicate over the bus using a
42
+ well-documented binary packet protocol (official reference repositories linked in
43
+ [section 15](#15-where-to-find-protocol-references)).
44
+
45
+ **This project** is a Node-RED palette: a set of custom nodes that let a Node-RED flow
46
+ read and write to a live Velbus bus. It connects to the bus via a TCP gateway (either
47
+ Velbus's own `velbus-tcp` snap package, or the open-source `python-velbustcp` project —
48
+ both simply expose the same underlying serial/CAN bus protocol over a TCP socket). Once
49
+ connected, each node in this palette represents one physical module (or, for a couple of
50
+ nodes, a bus-wide function) and translates between Velbus's binary packets and normal
51
+ Node-RED JSON messages.
52
+
53
+ **Two module generations exist** and are handled throughout this codebase as a first-class
54
+ distinction:
55
+ - **Original series** — the older module generation. Simpler protocol, no firmware/CAN FD
56
+ concept.
57
+ - **V2 / "-20" series** — the current module generation (module type names ending `-20`,
58
+ e.g. `VMB4RYLD-20`). Richer protocol, CAN FD capable, firmware version checking.
59
+
60
+ Where a module type exists in both generations, this palette generally provides two
61
+ separate nodes (e.g. `velbus-relay` for original-series relays, `velbus-relay-20` for
62
+ V2 relays) rather than one node trying to handle both — see
63
+ [section 9](#9-architecture-decisions-settled--do-not-reopen-casually) for the reasoning
64
+ and the specific exceptions to this rule.
65
+
66
+ ---
67
+
68
+ ## 2. Quick start
69
+
70
+ ```bash
71
+ # Install into an existing Node-RED instance
72
+ cd ~/.node-red
73
+ npm install node-red-contrib-velbus-2026
74
+ ```
75
+
76
+ Then, in the Node-RED editor:
77
+ 1. Drag a **velbus-bridge** config node onto a flow (or create one via any other Velbus
78
+ node's config panel) — set the host/port of your Velbus TCP gateway.
79
+ 2. Drag a **velbus-scan** node, wire an inject node into it, deploy, and fire it — this
80
+ performs a full bus scan and populates every other node's address dropdown with the
81
+ modules it finds.
82
+ 3. Add nodes for the specific modules on your bus (see [section 5](#5-complete-node-reference)
83
+ for the full list), select their address from the now-populated dropdown, and deploy.
84
+
85
+ No Velbus hardware to hand? See [section 11](#11-testing-without-real-hardware--the-mock-harness-pattern)
86
+ for how development and testing have been done without a live bus.
87
+
88
+ ---
89
+
90
+ ## 3. Repository structure
91
+
92
+ ```
93
+ package.json npm package definition — the node-red.nodes map here is how
94
+ Node-RED discovers every node; a new node must be registered here
95
+ index.js Intentionally minimal — Node-RED finds nodes via package.json, not this file
96
+ README.md User-facing installation and usage documentation
97
+ CHANGELOG_FORUM.md Full version-by-version development history — read this before
98
+ assuming something hasn't been tried or fixed already
99
+ LICENSE MIT
100
+
101
+ lib/ Shared code — protocol utilities and per-module-family type registries
102
+ velbus-utils.js chk() checksum, pkt()/rtrPkt() packet builders, parsePkt(),
103
+ splitPackets() — the packet framing primitives every node uses
104
+ relay-types.js Original-series relay module type registry
105
+ relay-types-20.js V2 relay module type registry
106
+ dimmer-types.js Original-series dimmer module type registry
107
+ dimmer-types-20.js V2 dimmer module type registry (incl. LED grouping mode / Device
108
+ Type table for VMB4LEDPWM-20)
109
+ glass-panel-types.js All 26 glass panel module types in one registry (hasOled/hasPir/
110
+ hasOc/minMapVer flags per type)
111
+ pir-types.js / pir-types-20.js PIR sensor module type registries
112
+ sensor-types.js / sensor-types-20.js Sensor/meteo module type registries
113
+ energy-types-20.js Power-supply-manager module type registry (single V2 type,
114
+ VMBPSUMNGR-20)
115
+ blind-types.js / blind-types-s.js / blind-types-20.js Blind/shutter module type registries
116
+ (three files because the blind family splits by protocol
117
+ capability, not simply by generation — see section 9)
118
+
119
+ nodes/ One directory per node, each containing a .js (logic) and
120
+ .html (editor UI + in-editor help) file of the same name
121
+ velbus-bridge/ Config node — the only node that holds the actual TCP connection
122
+ velbus-scan/ Full bus scanner
123
+ velbus-relay/ , velbus-relay-20/
124
+ velbus-dimmer/ , velbus-dimmer-20/
125
+ velbus-glass-panel/
126
+ velbus-thermostat/
127
+ velbus-button/
128
+ velbus-pir/ , velbus-pir-20/
129
+ velbus-meteo/
130
+ velbus-sensor/ , velbus-sensor-20/
131
+ velbus-blind/ , velbus-blind-s/ , velbus-blind-20/
132
+ velbus-clock/ Broadcasts system time/date/DST and clock alarms — the one
133
+ node with no fixed module address, see section 5
134
+ velbus-energy/ VMBPSUMNGR-20 power supply manager — PSU load, alarms,
135
+ warranty counter, per-rail wattage/voltage/amperage
136
+
137
+ examples/
138
+ velbus-basic-relay-dimmer.json A minimal example flow, importable directly in Node-RED
139
+ ```
140
+
141
+ **Convention for adding a new node:** copy the structure of the most similar existing node
142
+ (same module family or same generation) rather than starting from a blank file — the
143
+ boilerplate (bridge lookup, packet registration, status bar handling, firmware check for
144
+ V2 nodes) is consistent across every node and easy to get subtly wrong if rebuilt from
145
+ scratch.
146
+
147
+ ---
148
+
149
+ ## 4. Architecture overview
150
+
151
+ ### 4.1 The bridge is the only thing that touches the network
152
+
153
+ `velbus-bridge` is a Node-RED **config node** (shared, not visible as a flow node itself)
154
+ that owns the actual TCP socket to the Velbus gateway. It handles:
155
+ - Connecting (plain TCP or TLS, with optional auth-key handshake for `python-velbustcp`)
156
+ - Auto-reconnect on disconnect (5 second backoff)
157
+ - Splitting the raw TCP byte stream into individual packets (`splitPackets()` in
158
+ `lib/velbus-utils.js` — a stream doesn't respect packet boundaries, so this has to
159
+ track a remainder buffer across multiple `data` events)
160
+ - Dispatching each parsed packet to whichever node(s) have registered interest in that
161
+ module address (`bridge.register(address, callback)` / `bridge.deregister(...)`)
162
+ - A **scan lock** mechanism: while a bus scan is in progress, other nodes' startup RTR
163
+ (request-to-respond) packets are queued rather than sent immediately, and flushed
164
+ one-per-second once the scan completes — prevents startup traffic from colliding with
165
+ an active scan
166
+ - Serving persisted scan results to the editor's config dialogs, via an HTTP endpoint:
167
+ `GET /velbus/scan-results?bridge=<bridge-node-id>` → `{ modules: [...], count: N }`.
168
+ **The endpoint returns an object, not an array — always use `results.modules`, not
169
+ `results` directly.** This has been a real, repeated source of bugs.
170
+ - Rebuilding its subaddress-to-primary-address map from persisted scan results on
171
+ startup, so subaddress routing survives a Node-RED restart without requiring a fresh
172
+ scan every time.
173
+ - A public `isConnected()` method other nodes can check before sending, to give a clear
174
+ "not connected" status rather than a generic dropped-packet warning.
175
+
176
+ Every other node looks up its configured bridge via `RED.nodes.getNode(config.bridge)`,
177
+ registers for its own module address's packets, and sends outgoing packets via
178
+ `node.bridge.send(buffer)`.
179
+
180
+ ### 4.2 The packet protocol
181
+
182
+ Every Velbus packet, at the byte level, has this shape:
183
+
184
+ ```
185
+ 0F [pri] [addr] [dlc] [body...] [chk] 04
186
+ ```
187
+
188
+ | Field | Meaning |
189
+ |---|---|
190
+ | `0x0F` | Start-of-frame marker (fixed) |
191
+ | `pri` | Priority byte — see below |
192
+ | `addr` | Target/source module address (`0x00` = broadcast, `0x01`–`0xFE` = a real module) |
193
+ | `dlc` | Data length (low 4 bits = body byte count), or `0x40` for an RTR (request) packet |
194
+ | `body...` | The actual command/data bytes — see [section 4.3](#43-the-body-indexing-rule) |
195
+ | `chk` | Checksum — two's complement of the sum of all preceding bytes, mod 256 |
196
+ | `0x04` | End-of-frame marker (fixed) |
197
+
198
+ **Priority byte values** (confirmed against the official `packetprotocol` repository —
199
+ see [section 15](#15-where-to-find-protocol-references)):
200
+
201
+ | Value | Meaning |
202
+ |---|---|
203
+ | `0xF8` | High priority |
204
+ | `0xF9` | Firmware-related |
205
+ | `0xFA` | Third-party |
206
+ | `0xFB` | Low priority |
207
+
208
+ In practice, this codebase's convention (established after a real, painful bug — see
209
+ `CHANGELOG_FORUM.md` v0.7.0) is: **`0xFB` for status requests (`0xFA` command) and RTR
210
+ scan packets; `0xF8` for essentially everything else**, including outgoing commands and
211
+ most spontaneous module broadcasts. This is a convention observed to match real hardware
212
+ behaviour, not a rule stated explicitly and completely in the official docs — when in
213
+ doubt for a new packet type, check the specific module's protocol PDF for its actual
214
+ priority bits rather than assuming.
215
+
216
+ An RTR (bus scan request) packet has no body at all: `0F FB [addr] 40 [chk] 04`.
217
+
218
+ **Checksum** — sum every byte from the start marker through the last body byte
219
+ (mod 256), then take the two's complement:
220
+ ```javascript
221
+ function chk(bytes) {
222
+ let s = 0;
223
+ for (const x of bytes) s = (s + x) & 0xFF;
224
+ return ((~s + 1) & 0xFF);
225
+ }
226
+ ```
227
+ This exact function lives in `lib/velbus-utils.js` as `chk()`, used by `pkt()` (builds an
228
+ outgoing packet) and implicitly verified by `parsePkt()` when reading incoming ones.
229
+
230
+ ### 4.3 The body indexing rule — the single most common source of bugs
231
+
232
+ `parsePkt()` returns a `body` array where **`body[0]` is always the command byte itself**
233
+ (what the official protocol PDFs call DATABYTE1). Actual data starts at `body[1]`
234
+ (DATABYTE2).
235
+
236
+ ```
237
+ PDF says "DATABYTE2 = channel number" → code should read body[1] ✓
238
+ → code reading body[2] is WRONG (that's DATABYTE3)
239
+ ```
240
+
241
+ This off-by-one is the single most common mistake made throughout this project's
242
+ development history (see `CHANGELOG_FORUM.md` for several real instances). When
243
+ implementing a new packet handler, always write out the DATABYTE-to-`body[]` index
244
+ mapping explicitly as a comment before writing the parsing logic — it is not something
245
+ to trust from memory or infer quickly.
246
+
247
+ ### 4.4 Address format
248
+
249
+ Module addresses are stored as **hex strings in the editor's dropdown UI** (e.g.
250
+ `"0x0D — VMBELO"`) but **stored in the actual node config as a decimal integer**.
251
+ Numbers appearing in an old saved flow (from before hex-string dropdowns existed) should
252
+ still be treated as decimal for backward compatibility; new saves from the current editor
253
+ use hex strings. Both are handled by the same address-parsing logic in each node — if
254
+ you're seeing an address parse incorrectly, check whether the value arrived as a number
255
+ or a string before assuming the parsing logic itself is wrong.
256
+
257
+ ---
258
+
259
+ ## 5. Complete node reference
260
+
261
+ | Node | Category | Covers |
262
+ |---|---|---|
263
+ | `velbus-bridge` | config | The TCP connection itself — see section 4.1 |
264
+ | `velbus-scan` | Velbus (inputs) | Full bus scan, populates every other node's address dropdown |
265
+ | `velbus-relay` | Velbus (outputs) | Original-series relays: VMB1RY, VMB4RY, VMB4RYLD, VMB4RYNO, VMB1RYNO, VMB1RYNOS, VMB1RYS, VMB4RYLD-10, VMB4RYNO-10 |
266
+ | `velbus-relay-20` | Velbus (outputs) | V2 relays: VMB1RYS-20, VMB4RYLD-20, VMB4RYNO-20 |
267
+ | `velbus-dimmer` | Velbus (outputs) | Original-series dimmers: VMBDMI, VMBDMI-R, VMB4DC |
268
+ | `velbus-dimmer-20` | Velbus (outputs) | V2 dimmers: VMB2DC-20, VMB8DC-20, VMB4LEDPWM-20 (incl. RGB/RGBW grouping mode) |
269
+ | `velbus-glass-panel` | Velbus (inputs) | All 26 glass panel types (original + V2), buttons/OLED/PIR/open-collector as applicable per type |
270
+ | `velbus-thermostat` | Velbus (inputs) | Thermostat function on any glass panel module that has one — same address as the corresponding glass-panel node, coexists without conflict |
271
+ | `velbus-button` | Velbus (inputs) | Dedicated push-button modules: VMB8PB, VMB8PBU, VMB6PBN, VMB2PBN, VMB4PB, VMB6PB-20 |
272
+ | `velbus-pir` | Velbus (inputs) | Original-series PIR: VMBPIRO-10, VMBPIRM, VMBPIRC, VMBPIRO |
273
+ | `velbus-pir-20` | Velbus (inputs) | V2 PIR: VMBPIR-20, VMBPIRO-20 |
274
+ | `velbus-meteo` | Velbus (inputs) | Weather station: VMBMETEO |
275
+ | `velbus-sensor` | Velbus (inputs) | Original-series input/analogue: VMB7IN, VMB4AN |
276
+ | `velbus-sensor-20` | Velbus (inputs) | V2 input: VMB8IN-20 |
277
+ | `velbus-blind` | Velbus (outputs) | VMB1BL, VMB2BL |
278
+ | `velbus-blind-s` | Velbus (outputs) | VMB1BLS, VMB2BLE, VMB2BLE-10 |
279
+ | `velbus-blind-20` | Velbus (outputs) | VMB2BLE-20 |
280
+ | `velbus-clock` | Velbus (outputs) | **No fixed module address.** Broadcasts system time/date/DST to the bus broadcast address (`0x00`), and sets clock alarms either globally (broadcast) or locally (a specific module, via a per-message address override) |
281
+ | `velbus-energy` | Velbus (inputs) | VMBPSUMNGR-20 — power supply manager: PSU load percentages, live wattage/voltage/amperage per rail, a warranty (hours-in-operation) counter, and PSU/warranty alarm status |
282
+
283
+ Palette group colours: **Velbus (inputs)** is teal (`#3A8C8C`), **Velbus (outputs)** is
284
+ blue (`#4A90D9`).
285
+
286
+ ---
287
+
288
+ ## 6. Complete module type registry
289
+
290
+ ### Relays
291
+ | Type byte | Module | Node |
292
+ |---|---|---|
293
+ | 0x02 | VMB1RY | velbus-relay |
294
+ | 0x08 | VMB4RY | velbus-relay |
295
+ | 0x0D | VMB1RYS-20 | velbus-relay-20 |
296
+ | 0x10 | VMB4RYLD | velbus-relay |
297
+ | 0x11 | VMB4RYNO | velbus-relay |
298
+ | 0x1B | VMB1RYNO | velbus-relay |
299
+ | 0x26 | VMB4RYLD-20 | velbus-relay-20 |
300
+ | 0x27 | VMB4RYNO-20 | velbus-relay-20 |
301
+ | 0x29 | VMB1RYNOS | velbus-relay |
302
+ | 0x41 | VMB1RYS | velbus-relay |
303
+ | 0x48 | VMB4RYLD-10 | velbus-relay |
304
+ | 0x49 | VMB4RYNO-10 | velbus-relay |
305
+
306
+ ### Dimmers
307
+ | Type byte | Module | Node | LED grouping mode |
308
+ |---|---|---|---|
309
+ | 0x06 | VMB4LEDPWM-20 | velbus-dimmer-20 | single / rgb / rgbw (configurable) |
310
+ | 0x12 | VMB4DC | velbus-dimmer | n/a |
311
+ | 0x15 | VMBDMI | velbus-dimmer | n/a |
312
+ | 0x24 | VMB2DC-20 | velbus-dimmer-20 | n/a |
313
+ | 0x2F | VMBDMI-R | velbus-dimmer | n/a |
314
+ | 0x4B | VMB8DC-20 | velbus-dimmer-20 | n/a |
315
+
316
+ ### Push buttons
317
+ | Type byte | Module | Node |
318
+ |---|---|---|
319
+ | 0x01 | VMB8PB | velbus-button |
320
+ | 0x16 | VMB8PBU | velbus-button |
321
+ | 0x17 | VMB6PBN | velbus-button |
322
+ | 0x18 | VMB2PBN | velbus-button |
323
+ | 0x44 | VMB4PB | velbus-button |
324
+ | 0x4C | VMB6PB-20 | velbus-button |
325
+
326
+ ### Glass panels (all → velbus-glass-panel, 26 types)
327
+ | Type byte | Module | OLED | PIR | Open collector | Min. map version |
328
+ |---|---|---|---|---|---|
329
+ | 0x1E | VMBGP1 | no | no | unconfirmed¹ | — |
330
+ | 0x1F | VMBGP2 | no | no | unconfirmed¹ | — |
331
+ | 0x20 | VMBGP4 | no | no | unconfirmed¹ | — |
332
+ | 0x21 | VMBGPO | yes | no | yes | 2 |
333
+ | 0x2D | VMBGP4PIR | no | yes | unconfirmed¹ | — |
334
+ | 0x34 | VMBEL1 | no | no | yes | 2 |
335
+ | 0x35 | VMBEL2 | no | no | yes | 2 |
336
+ | 0x36 | VMBEL4 | no | no | yes | 2 |
337
+ | 0x37 | VMBELO | yes | no | yes | 4 |
338
+ | 0x38 | VMBELPIR | no | yes | yes | 2 |
339
+ | 0x3A | VMBGP1-2 | no | no | no² | — |
340
+ | 0x3B | VMBGP2-2 | no | no | no² | — |
341
+ | 0x3C | VMBGP4-2 | no | no | no² | — |
342
+ | 0x3D | VMBGPOD-2 | yes | no | unconfirmed¹ | 2 |
343
+ | 0x47 | VMBEL2PIR | no | yes | yes | — |
344
+ | 0x4F | VMBEL1-20 | no | no | yes | — |
345
+ | 0x50 | VMBEL2-20 | no | no | yes | — |
346
+ | 0x51 | VMBEL4-20 | no | no | yes | — |
347
+ | 0x52 | VMBELO-20 | yes | no | yes | — |
348
+ | 0x53 | VMBELPIR-20³ | no | yes | yes | — |
349
+ | 0x54 | VMBGP1-20 | no | no | unconfirmed¹ | — |
350
+ | 0x55 | VMBGP2-20 | no | no | unconfirmed¹ | — |
351
+ | 0x56 | VMBGP4-20 | no | no | unconfirmed¹ | — |
352
+ | 0x57 | VMBGPO-20 | yes | no | yes | — |
353
+ | 0x5C | VMBEL2PIR-20³ | no | yes | yes | — |
354
+ | 0x5F | VMBGP4PIR-20 | no | yes | unconfirmed¹ | — |
355
+
356
+ ¹ Open-collector support unconfirmed against real hardware — see [section 13](#13-known-open-issues).
357
+ ² Confirmed no open-collector commands in the protocol PDF, but not yet live-verified.
358
+ ³ The official Velbus type list shows different module names at these two type bytes than
359
+ what this registry currently uses — flagged for verification, see
360
+ [section 13](#13-known-open-issues).
361
+
362
+ ### PIR sensors
363
+ | Type byte | Module | Node | Has temp. sensor |
364
+ |---|---|---|---|
365
+ | 0x23 | VMBPIRO-10 | velbus-pir | yes |
366
+ | 0x2A | VMBPIRM | velbus-pir | no |
367
+ | 0x2B | VMBPIRC | velbus-pir | no |
368
+ | 0x2C | VMBPIRO | velbus-pir | yes |
369
+ | 0x4D | VMBPIR-20 | velbus-pir-20 | no |
370
+ | 0x59 | VMBPIRO-20 | velbus-pir-20 | yes |
371
+
372
+ ### Sensors / meteo
373
+ | Type byte | Module | Node |
374
+ |---|---|---|
375
+ | 0x22 | VMB7IN | velbus-sensor |
376
+ | 0x31 | VMBMETEO | velbus-meteo |
377
+ | 0x32 | VMB4AN | velbus-sensor |
378
+ | 0x4E | VMB8IN-20 | velbus-sensor-20 |
379
+
380
+ ### Blind / shutter
381
+ | Type byte | Module | Node |
382
+ |---|---|---|
383
+ | 0x03 | VMB1BL | velbus-blind |
384
+ | 0x09 | VMB2BL | velbus-blind |
385
+ | 0x1D | VMB2BLE | velbus-blind-s |
386
+ | 0x2E | VMB1BLS | velbus-blind-s |
387
+ | 0x4A | VMB2BLE-10 | velbus-blind-s |
388
+ | 0x61 | VMB2BLE-20 | velbus-blind-20 |
389
+
390
+ ### Energy / infrastructure
391
+ | Type byte | Module | Node |
392
+ |---|---|---|
393
+ | 0x04 | VMBPSUMNGR-20 | velbus-energy |
394
+
395
+ ---
396
+
397
+ ## 7. Critical protocol knowledge
398
+
399
+ ### 7.1 Firmware build number — the official docs mislabel this field
400
+
401
+ In the `0xFF` module-identification response, bytes labelled "Build Year" and
402
+ "Build Week" in the official protocol PDFs are, in every module tested against real
403
+ hardware, actually the **high and low bytes of a single build number**, not a
404
+ year/week pair:
405
+
406
+ ```
407
+ build = (body[5] × 100) + body[6]
408
+ Example: body[5]=0x24 (36 decimal), body[6]=0x36 (54 decimal) → (36×100)+54 = 3654
409
+ ```
410
+
411
+ ### 7.2 `0xFF` module type/identification response
412
+ ```
413
+ body[0] = 0xFF
414
+ body[1] = module type byte
415
+ body[2] = serial number, high byte
416
+ body[3] = serial number, low byte
417
+ body[4] = memory map version
418
+ body[5] = build number high byte (decimal, see 7.1)
419
+ body[6] = build number low byte (decimal, see 7.1)
420
+ body[7] = properties byte — V2 modules only (bit 5 = CAN FD capable, bit 0 = terminator fitted)
421
+ ```
422
+ Original-series modules: 7 bytes total (no `body[7]`).
423
+ Exceptions confirmed against real hardware: **VMB1BL/VMB2BL** send only 5 bytes (no
424
+ serial number; `body[2]` is instead a DIP-switch timeout setting). **VMBELO**, despite
425
+ being an original-series module, sends the full 8-byte V2-style response.
426
+ **VMB2BLE-10** also sends 8 bytes, but `body[7]` there is only a terminator flag, not
427
+ the full V2 properties byte.
428
+
429
+ ### 7.3 `0xB0` subaddress response
430
+ ```
431
+ body[0] = 0xB0
432
+ body[1] = module type
433
+ body[2-3] = serial number
434
+ body[4-7] = up to four subaddresses (0xFF = subaddress slot disabled/unused)
435
+ ```
436
+ Subaddresses are **source addresses for incoming events only** — outgoing commands
437
+ always go to the module's primary address, never to a subaddress. The bridge handles
438
+ `0xB0` responses passively, building a subaddress→primary-address map so that other
439
+ nodes don't need any subaddress-specific logic themselves. A `0xB0` response can arrive
440
+ up to ~500ms after the initial `0xFF` — always allow for this delay before treating a
441
+ module's discovery as complete.
442
+
443
+ ### 7.4 `0xFB` relay status — two completely different formats depending on generation
444
+
445
+ **Original series** — one packet per active channel:
446
+ ```
447
+ body[0] = 0xFB
448
+ body[1] = channel number
449
+ body[2] = state (0 = off, 1 = on)
450
+ body[3-5] = 24-bit timer remaining, in seconds
451
+ body[6] = LED state
452
+ ```
453
+ Since one packet is sent per channel, allow up to ~200ms to collect all of them for a
454
+ multi-channel module.
455
+
456
+ **V2 series** — a single bitmask packet covering every channel at once:
457
+ ```
458
+ body[0] = 0xFB
459
+ body[1] = active (on) bitmask
460
+ body[2] = timer-running bitmask
461
+ body[3] = forced bitmask
462
+ body[4] = inhibited bitmask
463
+ body[5] = locked bitmask
464
+ ```
465
+
466
+ ### 7.5 `0xEE` dimmer status — V2 series (all three -20 dimmer types)
467
+ ```
468
+ body[0] = 0xEE
469
+ body[1] = on/off bitmask
470
+ body[2] = inhibited bitmask
471
+ body[3] = forced-on bitmask
472
+ body[4] = forced-off bitmask
473
+ body[5] = program-disabled bitmask
474
+ body[6] = error bitmask
475
+ body[7] = alarm/program byte
476
+ ```
477
+
478
+ ### 7.6 `0xA5` dim level — up to four channels packed per packet
479
+ ```
480
+ body[0] = 0xA5, body[1] = channel, body[2] = level (0-254)
481
+ [optionally repeated: body[3] = channel, body[4] = level, ...]
482
+ ```
483
+
484
+ ### 7.7 `0x1E` RGBW command — VMB4LEDPWM-20 in rgb/rgbw grouping mode only
485
+ ```
486
+ body[0] = 0x1E
487
+ body[1] = group (0xFF = all)
488
+ body[2] = R, body[3] = G, body[4] = B, body[5] = W
489
+ body[6] = fade mode (0 = direct, 1 = rate, 2 = time)
490
+ ```
491
+
492
+ **Watch this one carefully:** command bytes `0x12`/`0x14` mean **forced OFF / forced ON**
493
+ on relay nodes, but mean **forced UP / forced DOWN** on blind nodes. The same byte value
494
+ means something entirely different depending on module type — always check module type
495
+ before reusing a command byte pattern from a different node family.
496
+
497
+ ### 7.8 Memory read/write timing constraints
498
+ ```
499
+ Original series: minimum 20ms between 0xFC writes — this is real EEPROM with a
500
+ finite write-cycle lifespan, do not write faster than this
501
+ V2 series: wait for the module's 0xFE acknowledgement before the next write
502
+ OLED framebuffer: 10ms minimum per 4-byte block (V2 modules with a display)
503
+ Address assignment: allow 500ms, then rescan to confirm the new address took effect
504
+ ```
505
+ **Never write in response to a real-time bus event** — an incoming status broadcast is
506
+ not the right trigger for an automatic write-back, both for the EEPROM-wear reason above
507
+ and because it risks a feedback loop. Reads have no such constraint and are always safe.
508
+
509
+ ### 7.9 Thermostat commands — always to the primary address
510
+ ```
511
+ 0xDB = comfort mode, 0xDC = day mode, 0xDD = night mode, 0xDE = safe mode
512
+ 0xE0 = heat mode, 0xDF = cool mode
513
+ 0xE4 = set target temperature, 0xE5 = request temperature settings, 0xE7 = request status
514
+ 0xEF = request module name (append a channel byte)
515
+ 0xFA = request module status
516
+ ```
517
+ Thermostat commands go to the module's **primary address only**, never to a thermostat
518
+ subaddress, even though the thermostat function itself may be logically associated with
519
+ a subaddress in the module's own internal architecture.
520
+
521
+ ### 7.10 Temperature encoding — two different formats in use
522
+
523
+ ```javascript
524
+ // Signed byte × 0.5°C — used in the 0xE8 thermostat settings packet
525
+ function signed05(b) { return (b > 127 ? b - 256 : b) * 0.5; }
526
+
527
+ // Signed 16-bit, divided by 512 — used in the 0xE6 temperature packet
528
+ // (0.0625°C resolution). NOTE: official protocol documentation shows a
529
+ // divisor of 16 for this field — that is wrong. It has been directly
530
+ // verified against real hardware that the correct divisor is 512.
531
+ function tempFrom16(hi, lo) {
532
+ const raw = (hi << 8) | lo;
533
+ return (raw > 32767 ? raw - 65536 : raw) / 512;
534
+ }
535
+ ```
536
+ This divisor discrepancy (16 vs. 512) is exactly the kind of thing worth re-verifying
537
+ against a real module if you're implementing a new node that touches `0xE6` — don't
538
+ trust the official PDF's stated divisor for this specific field without checking.
539
+
540
+ ---
541
+
542
+ ## 8. Payload conventions
543
+
544
+ Every node's Node-RED output message follows the same conventions, regardless of module
545
+ type:
546
+
547
+ - **Numbers are always numbers, never strings.** A temperature is `21.5`, not `"21.5"`.
548
+ - **An `on` boolean is present only where a meaningful binary on/off state actually
549
+ exists.** Temperature, counter, and settings-only payloads have no `on` field at all —
550
+ don't add one just for consistency's sake if the underlying value isn't truly binary.
551
+ - **`topic`** identifies the payload shape/purpose (e.g. `"relay_status"`,
552
+ `"dimmer_status"`, `"button"`, `"thermostat_status"`) so a single debug/switch node
553
+ downstream can distinguish message types without inspecting every field.
554
+
555
+ Representative examples:
556
+
557
+ ```json
558
+ { "topic": "relay_status", "address": "0x10", "module": "Hall Relay",
559
+ "channel": 1, "state": "on", "on": true, "timerRemaining": 0 }
560
+
561
+ { "topic": "dimmer_status", "address": "0xA5", "module": "GF Lights",
562
+ "channel": 1, "state": "on", "on": true, "level": 187, "percent": 73.6,
563
+ "outputType": "PWM", "dimCurve": "exponential", "ledMode": "single" }
564
+
565
+ { "topic": "button", "address": "0x03", "module": "Hall Panel",
566
+ "type": "button", "on": true,
567
+ "pressed": [1, 3], "released": [], "longPressed": [] }
568
+
569
+ { "topic": "thermostat_status", "type": "thermostat",
570
+ "currentTemp": 21.5, "targetTemp": 22.0, "mode": "comfort",
571
+ "heaterMode": true, "heating": false, "thermostatOn": true }
572
+
573
+ { "topic": "temperature", "type": "temperature",
574
+ "current": 21.5, "min": 15.0, "max": 30.0 }
575
+
576
+ { "topic": "meteo", "type": "meteo",
577
+ "rain": 0.5, "light": 12500, "wind": 14.3 }
578
+
579
+ { "topic": "blind_status", "type": "blind_status",
580
+ "channel": 1, "on": true, "status": "up", "position": 25,
581
+ "lockState": "normal", "autoMode": 0 }
582
+
583
+ { "topic": "module_online", "address": "0x06", "module": "VMB4LEDPWM-20",
584
+ "typeId": "0x06", "outputType": "PWM", "dimCurve": "exponential",
585
+ "ledMode": "single", "channels": 4, "build": 2436, "canFD": false }
586
+ ```
587
+
588
+ **Node status bar format** (shown under each node in the Node-RED editor): `"{module
589
+ name} (0x{address}) {state}"`. Name priority, highest first: an explicit user override
590
+ in node config, then the module's own name as reported by VelbusLink/read from the
591
+ module itself, then falling back to the generic module type string if neither is
592
+ available.
593
+
594
+ ---
595
+
596
+ ## 9. Architecture decisions (settled — do not reopen casually)
597
+
598
+ These are decisions made deliberately, generally after real back-and-forth about the
599
+ alternatives. Reopening one isn't forbidden, but should be a considered choice, not an
600
+ accidental drift — if you find yourself changing one of these, it's worth being explicit
601
+ about why in the commit message.
602
+
603
+ - **Generational split.** Nodes generally split at the V2.0 boundary — one node for
604
+ original-series, a separate node for V2. **Exceptions:** `velbus-glass-panel` (one
605
+ node covers all 26 types, both generations, via the type registry's per-type flags
606
+ rather than a code-level split), `velbus-thermostat` (covers the thermostat function
607
+ on any glass panel type), `velbus-meteo` (only one generation exists), and the blind
608
+ family (`velbus-blind` / `velbus-blind-s` / `velbus-blind-20`, split by actual protocol
609
+ capability rather than strictly by generation, since some non-V2 blind modules share
610
+ more protocol in common with certain V2 ones than with other original-series ones).
611
+ - **Firmware check (V2 nodes only).** A three-stage check on receiving the module's
612
+ `0xFF` identification: type byte matches → memory map version meets the node's stated
613
+ minimum → pass. Failure is a hard block with a red status, not a soft warning.
614
+ Original-series nodes skip this check entirely (no firmware/map-version concept
615
+ exists for them).
616
+ - **Name auto-retrieval.** V2 nodes, and the glass-panel/thermostat nodes, request the
617
+ module's name (`0xEF`) automatically on startup, with a 2-second timeout — whatever
618
+ has been received by then is used, even if the name is incomplete.
619
+ - **Scan lock.** `velbus-scan` calls the bridge's scan-lock before scanning; every other
620
+ node's own startup RTR requests are queued during an active scan and flushed at a rate
621
+ of one per second once scanning completes, rather than being sent immediately and
622
+ risking a collision with in-progress scan traffic.
623
+ - **Address dropdown data source.** Always read `results.modules`, never `results`
624
+ directly, from the bridge's `/velbus/scan-results` endpoint — it returns an object
625
+ wrapping the array, not the array itself. Real, repeated source of bugs — worth
626
+ restating even though it's also in section 4.1.
627
+ - **Thermostat commands always target the primary address**, never a subaddress, even
628
+ though the thermostat function may be logically tied to one internally.
629
+ - **`0xFF` response body length** is 7 bytes on original-series modules, 8 bytes on V2
630
+ — except VMBELO (original series, but sends 8 bytes) and VMB2BLE-10 (sends 8 bytes,
631
+ but the 8th is only a terminator flag, not full V2 properties). See section 7.2.
632
+ - **VMB4LEDPWM-20 LED grouping mode** is a node config property that documents which
633
+ mode (single/rgb/rgbw) the physical module is wired for — the node does **not** write
634
+ this setting to the module automatically. Changing a module's grouping mode is a
635
+ deliberate commissioning-time decision made by a human (or a dedicated commissioning
636
+ tool), not something a live command node should do silently. A read-only
637
+ `get_device_type` command exists on `velbus-dimmer-20` to verify the module's actual
638
+ setting matches the node's configured value.
639
+ - **`velbus-clock`'s `set_alarm` command accepts a per-message address override**,
640
+ breaking from the "one node = one fixed module address" pattern every other node
641
+ follows. This was a deliberate exception, not an oversight: the underlying protocol
642
+ command (`0xC3`, clock alarm) is identical whether sent globally (broadcast) or
643
+ locally (one specific module) — differing only in destination address — and is a
644
+ shared system-level command across V2 module types generally, not a feature tied to
645
+ one specific module type. Putting per-module alarm handling on every relevant node
646
+ type instead would mean duplicating an identical handler several times over. This is
647
+ flagged as a considered judgement call rather than a permanently settled decision —
648
+ if it later seems better as a proper per-module config field (matching every other
649
+ node's pattern), that's a reasonable direction to switch to.
650
+
651
+ ---
652
+
653
+ ## 10. Development environment setup
654
+
655
+ **Prerequisites:**
656
+ - Node.js ≥ 14
657
+ - Node-RED ≥ 2.0.0
658
+ - A Velbus TCP gateway of some kind — either the official `velbus-tcp` package, or the
659
+ open-source `python-velbustcp` project (see [section 15](#15-where-to-find-protocol-references)
660
+ for links). Both simply expose the same bus protocol over a TCP socket; this palette
661
+ doesn't care which one it's talking to.
662
+ - Ideally, physical Velbus hardware to test against — though a great deal of useful
663
+ development and verification is possible without it; see
664
+ [section 11](#11-testing-without-real-hardware--the-mock-harness-pattern).
665
+
666
+ **For live development** (editing node code and seeing changes without repackaging a
667
+ tarball every time), symlink this repository directly into Node-RED's node_modules
668
+ rather than repeatedly installing from a tarball:
669
+ ```bash
670
+ cd ~/.node-red/node_modules
671
+ ln -s /path/to/your/local/checkout/node-red-contrib-velbus-2026 node-red-contrib-velbus-2026
672
+ ```
673
+ Restart Node-RED after each code change to pick it up (Node-RED does not hot-reload
674
+ node `.js` files). For `.html` (editor UI/help) changes specifically, a hard browser
675
+ refresh (`Ctrl+Shift+R` or equivalent) is usually enough, since the editor caches node
676
+ HTML client-side — a full Node-RED restart isn't always necessary for HTML-only changes,
677
+ but doesn't hurt if something isn't picking up.
678
+
679
+ **No physical Velbus hardware and no gateway software set up yet?** You can still make
680
+ substantial progress — see the next section.
681
+
682
+ ---
683
+
684
+ ## 11. Testing without real hardware — the mock-harness pattern
685
+
686
+ A useful, repeatedly-proven technique for verifying a node's packet-building logic
687
+ *before* ever touching real hardware: write a small standalone script that mocks just
688
+ enough of the Node-RED `RED` object to load the node file directly, wire a fake bridge
689
+ that captures whatever bytes get sent, and hand-verify the resulting packet against the
690
+ protocol specification (checksum, DLC, byte-for-byte field layout).
691
+
692
+ This does **not** replace testing against real hardware — it catches packet-construction
693
+ bugs (wrong byte count, wrong checksum, off-by-one field indexing) before they ever reach
694
+ a bus, but it cannot confirm that a real module actually does what the protocol PDF says
695
+ it should do in response. Both kinds of verification matter; this one is simply always
696
+ available, with zero hardware dependency.
697
+
698
+ **Minimal example** (adapt the node path, config, and input payload to whatever you're
699
+ testing):
700
+
701
+ ```javascript
702
+ const handlers = {};
703
+
704
+ function MockNode() {
705
+ this.on = (evt, fn) => { handlers[evt] = fn; };
706
+ this.status = (s) => console.log('STATUS:', JSON.stringify(s));
707
+ this.warn = (m) => console.log('WARN:', m);
708
+ this.error = (m) => console.log('ERROR:', m);
709
+ this.send = (msg) => console.log('OUTPUT:', JSON.stringify(msg));
710
+ }
711
+
712
+ const sentPackets = [];
713
+ const mockBridge = {
714
+ isConnected: () => true,
715
+ send: (buf) => sentPackets.push(buf)
716
+ };
717
+
718
+ const RED = {
719
+ nodes: {
720
+ createNode: (self) => { Object.assign(self, new MockNode()); },
721
+ getNode: () => mockBridge,
722
+ registerType: (name, ctor) => { RED._ctor = ctor; }
723
+ }
724
+ };
725
+
726
+ require('/path/to/nodes/velbus-example/velbus-example.js')(RED);
727
+
728
+ const config = { bridge: 'x' /* , ...whatever config fields this node expects */ };
729
+ new RED._ctor(config);
730
+
731
+ // Fire whatever input message you want to test
732
+ handlers['input']({ payload: { cmd: 'some_command', /* ...fields */ } });
733
+
734
+ // Inspect the resulting bytes
735
+ for (const buf of sentPackets) {
736
+ console.log(buf.toString('hex').toUpperCase().match(/.{1,2}/g).join(' '));
737
+ }
738
+ ```
739
+
740
+ Hand-verify the printed hex bytes against the protocol PDF: confirm the DLC nibble
741
+ matches the actual body length, confirm each field lands at the expected offset, and
742
+ compute the checksum by hand (sum all bytes before it, mod 256, two's complement) to
743
+ confirm it matches what the code produced. This exact pattern has caught real bugs
744
+ before code ever reached a physical bus — worth running for any new packet-building
745
+ logic, not just as an afterthought.
746
+
747
+ ---
748
+
749
+ ## 12. Testing status — what's actually verified
750
+
751
+ Testing maturity **varies significantly by node** — do not assume something works
752
+ against real hardware just because it's present in the published package. The
753
+ authoritative, version-by-version record is `CHANGELOG_FORUM.md`; check the entries for
754
+ the specific node/command you're relying on before treating it as proven. As a general
755
+ orientation at the time of writing:
756
+
757
+ - **Field-tested against live hardware:** the core relay, dimmer, glass panel, and
758
+ thermostat nodes have real-hardware confirmation for their primary functions.
759
+ - **Mock-harness verified only, not yet confirmed against a real bus:** `velbus-clock`
760
+ (both the time/date/DST broadcast and the `set_alarm` command), the
761
+ `velbus-dimmer-20` `get_device_type` read command, and `velbus-energy` in its
762
+ entirety (no VMBPSUMNGR-20 has been confirmed present on a scanned bus yet).
763
+ - **Fixed but pending re-confirmation on hardware different from what it was originally
764
+ tested on:** several nodes had packet-construction bugs fixed in bulk during one
765
+ intensive debugging pass (see `CHANGELOG_FORUM.md`, the entries covering versions
766
+ 0.6.6 through 0.7.0) — confirm against whatever specific module you're working with
767
+ if you haven't personally seen it work.
768
+
769
+ ---
770
+
771
+ ## 13. Known open issues
772
+
773
+ - **Open-collector support** on several glass panel types (marked "unconfirmed" in the
774
+ registry in section 6) has not been verified against real hardware — the protocol PDFs
775
+ are ambiguous or silent on some of these. Sending `0x01`/`0x02`/`0x03` open-collector
776
+ commands to one of these modules and observing whether it responds would resolve this.
777
+ - **Module names at type bytes `0x53` and `0x5C`** — the official Velbus type list shows
778
+ different module names than what this registry currently uses for these two type
779
+ bytes. Needs verification against a real module or a VelbusLink bus scan.
780
+ - **`velbus-energy` verified only against a mock test harness** — no
781
+ VMBPSUMNGR-20 has actually been confirmed present on a scanned bus yet, only
782
+ assumed likely present at a known installation. Packet checksums and field
783
+ layout are hand-verified against the protocol PDF; real-hardware behaviour
784
+ is unconfirmed. The `0xA3` (PSU values) byte layout in particular was
785
+ reconstructed from an internally-inconsistent section of the official PDF
786
+ (see `CHANGELOG_FORUM.md`, v0.9.0) rather than read directly — the single
787
+ most likely spot for a real decode error to surface once tested live.
788
+ - **V2 relay interval/blink timer** — confirmed there is no live bus command for this on
789
+ V2-series relays at all (unlike the original series, which has one). A real interval
790
+ timer on a V2 relay requires writing a Program Step to the module's memory (protocol
791
+ command `0xC2`, Action code 22), which is a fundamentally different, more involved
792
+ kind of operation than any other command node in this palette currently performs.
793
+ Whether this belongs in this palette at all (as a new, more involved node) or is out
794
+ of scope is an open question, not a settled one.
795
+ - **`velbus-clock`'s multi-channel behaviour** during simultaneous multi-channel relay
796
+ blinking (interval timer) hasn't been explicitly tested for whether `channel`/
797
+ `channelBit` fields report correctly when more than one channel blinks at once.
798
+ - **DST auto-detection heuristic** in `velbus-clock` (comparing the current UTC offset
799
+ against the year's January/July offsets to infer whether daylight saving is active)
800
+ has only been sanity-checked in an environment where daylight saving never applies —
801
+ a genuine positive "DST is active" case hasn't been observed and confirmed correct.
802
+
803
+ ---
804
+
805
+ ## 14. Contribution and release workflow
806
+
807
+ **Every release, regardless of size:**
808
+
809
+ 1. **Bump the version** in `package.json` — npm refuses to publish an already-used
810
+ version number, even for a trivial fix. Add a matching entry to
811
+ `CHANGELOG_FORUM.md` describing what changed and, where relevant, *why* — the reasoning
812
+ behind a fix is often more valuable to a future reader than the diff itself.
813
+ 2. **Commit and push:**
814
+ ```bash
815
+ git add -A
816
+ git commit -m "Describe the change"
817
+ git push
818
+ ```
819
+ 3. **Publish to npm:**
820
+ ```bash
821
+ npm publish
822
+ ```
823
+
824
+ **Publishing requires either 2FA enabled on the npm account used, or a Granular Access
825
+ Token with "Bypass two-factor authentication" explicitly ticked** — npm's current
826
+ policy rejects publish attempts otherwise with a `403` error. Whoever is publishing
827
+ needs one or the other set up; this has been a real stumbling block in practice.
828
+
829
+ **Recommended, not required:** tag the release to match, so it's easy to see which
830
+ commit corresponds to which published version:
831
+ ```bash
832
+ git tag v<version>
833
+ git push --tags
834
+ ```
835
+
836
+ **When adding a new node:**
837
+ - Copy the closest existing equivalent as a starting structure rather than writing from
838
+ scratch — the bridge lookup, packet registration, and status-bar boilerplate is
839
+ consistent throughout and easy to get subtly wrong if reinvented.
840
+ - Register it in `package.json`'s `node-red.nodes` map — Node-RED will not discover a
841
+ node that exists as a file but isn't listed there.
842
+ - Verify the packet construction using the mock-harness pattern
843
+ ([section 11](#11-testing-without-real-hardware--the-mock-harness-pattern)) before
844
+ ever sending it to a real bus.
845
+ - Add its testing status honestly to `CHANGELOG_FORUM.md` — "verified via mock harness
846
+ only, not yet confirmed on real hardware" is a completely acceptable and expected
847
+ status for a new addition; the goal is accuracy, not appearing more finished than it is.
848
+
849
+ ---
850
+
851
+ ## 15. Where to find protocol references
852
+
853
+ - **`https://github.com/velbus/packetprotocol`** — the low-level packet framing
854
+ specification (start/end markers, priority byte values, checksum algorithm).
855
+ - **`https://github.com/velbus/moduleprotocol`** — per-module protocol PDFs, one (or a
856
+ few, where several similar modules share one document) per module type. This is the
857
+ authoritative source for any module-specific command/status byte layout — always
858
+ check the actual PDF for the specific module type you're working on rather than
859
+ assuming it matches a similar-looking one, given how many small but real differences
860
+ exist between superficially similar modules (see sections 7.2 and 7.10 for two
861
+ concrete examples of official documentation being subtly wrong or inconsistent).
862
+ - **`https://github.com/velbus/python-velbustcp`** — an open-source TCP gateway
863
+ implementation; one of two practical ways (alongside Velbus's own `velbus-tcp`
864
+ package) to get a real bus accessible over TCP for this palette to connect to.
865
+ - The Velbus community forum (search for "building custom velbus devices") has at least
866
+ one publicly documented from-scratch virtual-module implementation, which has proven a
867
+ useful cross-reference for less-obvious bit-level encoding questions (particularly
868
+ around relay status packet formats) beyond what the official PDFs spell out clearly.
869
+ - Other open-source Velbus integrations worth cross-referencing when a protocol question
870
+ isn't fully resolved by the official docs: the openHAB Velbus binding, and the
871
+ HomeAssistant `velbusaio` integration — both are mature, independently-implemented
872
+ interpretations of the same protocol, and occasionally clarify an ambiguity the
873
+ official PDFs leave unclear.
874
+
875
+ ---
876
+
877
+ ## 16. Code style rules
878
+
879
+ - **No pseudocode, ever.** Every function, file, and HTML block committed should be a
880
+ complete, working, drop-in replacement — never a partial sketch or a "fill this in"
881
+ skeleton.
882
+ - **Prove the logic before restructuring it.** Get a single packet working and verified
883
+ (even just via the mock harness) before refactoring for elegance or reuse.
884
+ - **`body[0]` is always the command byte.** Real data starts at `body[1]`. No
885
+ exceptions — see section 4.3 if this isn't already second nature.
886
+ - **Numbers are always numbers in payloads, never strings.**
887
+ - **`results.modules`, not `results`,** when reading the scan endpoint.
888
+ - **Thermostat commands go to the primary address only,** never a subaddress.
889
+ - **Respect the 20ms minimum between `0xFC` writes** on original-series modules — this
890
+ is real EEPROM wear, not an arbitrary rate limit.
891
+ - **British English** throughout documentation and in-editor help text (this is a
892
+ British-developed project for what is predominantly a European/UK installer base —
893
+ purely a style convention, not a functional requirement).
894
+
895
+ ---
896
+
897
+ ## 17. License and attribution
898
+
899
+ MIT licensed — see `LICENSE`.
900
+
901
+ Originally developed by Stuart Hanlon / MDAR Limited, the UK Velbus distributor
902
+ (`mdar.co.uk`), as a modern replacement for an earlier, unmaintained community Velbus
903
+ Node-RED palette. Protocol reverse-engineering and verification work throughout this
904
+ project has drawn on the official Velbus protocol repositories, the Velbus community
905
+ forum, and cross-referencing against other independent open-source Velbus integrations
906
+ — see [section 15](#15-where-to-find-protocol-references) for specific links.
package/README.md CHANGED
@@ -30,8 +30,11 @@ This is a ground-up rewrite replacing the abandoned
30
30
 
31
31
  - Node-RED v3.0 or later (tested on v5.0)
32
32
  - A Velbus TCP gateway:
33
- - [velbus-tcp snap](https://snapcraft.io/velbus-tcp) (default port 6000)
34
- - [python-velbustcp](https://github.com/velbus/python-velbustcp) (default port 27015)
33
+ - [velbus-tcp snap](https://snapcraft.io/velbus-tcp)
34
+ - [python-velbustcp](https://github.com/velbus/python-velbustcp)
35
+ - Velserv
36
+ - Velbus_PBserver
37
+ - Signum
35
38
  - Node.js 18+
36
39
 
37
40
  ---
@@ -12,7 +12,7 @@
12
12
  defaults: {
13
13
  name: { value: '' },
14
14
  bridge: { value: '', type: 'velbus-bridge', required: true },
15
- address: { value: '', required: true, validate: function(v) { return /^(0x)?[0-9a-fA-F]{1,2}$/.test(v); } },
15
+ address: { value: '', required: true, validate: function(v) { const n = parseInt(v); return !isNaN(n) && n >= 1 && n <= 254; } },
16
16
  channel: { value: 1, required: true },
17
17
  moduleName: { value: '' },
18
18
  typeId: { value: '' },
@@ -11,7 +11,7 @@
11
11
  defaults: {
12
12
  name: { value: '' },
13
13
  bridge: { value: '', type: 'velbus-bridge', required: true },
14
- address: { value: '', required: true, validate: function(v) { return /^(0x)?[0-9a-fA-F]{1,2}$/.test(v); } },
14
+ address: { value: '', required: true, validate: function(v) { const n = parseInt(v); return !isNaN(n) && n >= 1 && n <= 254; } },
15
15
  channel: { value: 1, required: true },
16
16
  moduleName: { value: '' },
17
17
  typeId: { value: '' },
@@ -13,7 +13,7 @@
13
13
  defaults: {
14
14
  name: { value: '' },
15
15
  bridge: { value: '', type: 'velbus-bridge', required: true },
16
- address: { value: '', required: true, validate: function(v) { return /^(0x)?[0-9a-fA-F]{1,2}$/.test(v); } },
16
+ address: { value: '', required: true, validate: function(v) { const n = parseInt(v); return !isNaN(n) && n >= 1 && n <= 254; } },
17
17
  channel: { value: 1, required: true },
18
18
  moduleName: { value: '' },
19
19
  typeId: { value: '' },
@@ -15,7 +15,7 @@
15
15
  defaults: {
16
16
  name: { value: '' },
17
17
  bridge: { value: '', type: 'velbus-bridge', required: true },
18
- address: { value: '', required: true, validate: function(v) { return /^(0x)?[0-9a-fA-F]{1,2}$/.test(v); } },
18
+ address: { value: '', required: true, validate: function(v) { const n = parseInt(v); return !isNaN(n) && n >= 1 && n <= 254; } },
19
19
  channelCount: { value: 8 },
20
20
  moduleName: { value: '' },
21
21
  typeId: { value: '' },
@@ -11,7 +11,7 @@
11
11
  defaults: {
12
12
  name: { value: '' },
13
13
  bridge: { value: '', type: 'velbus-bridge', required: true },
14
- address: { value: '', required: true, validate: function(v) { return /^(0x)?[0-9a-fA-F]{1,2}$/.test(v); } },
14
+ address: { value: '', required: true, validate: function(v) { const n = parseInt(v); return !isNaN(n) && n >= 1 && n <= 254; } },
15
15
  moduleName: { value: '' },
16
16
  typeId: { value: '' },
17
17
  },
@@ -135,7 +135,7 @@
135
135
  defaults: {
136
136
  name: { value: '' },
137
137
  bridge: { value: '', type: 'velbus-bridge', required: true },
138
- address: { value: '', required: true, validate: function(v) { return /^(0x)?[0-9a-fA-F]{1,2}$/.test(v); } },
138
+ address: { value: '', required: true, validate: function(v) { const n = parseInt(v); return !isNaN(n) && n >= 1 && n <= 254; } },
139
139
  channelCount: { value: 4, required: true, validate: RED.validators.number() },
140
140
  moduleName: { value: '' },
141
141
  typeId: { value: '' },
@@ -7,7 +7,7 @@
7
7
  defaults: {
8
8
  name: { value: '' },
9
9
  bridge: { value: '', type: 'velbus-bridge', required: true },
10
- address: { value: '', required: true, validate: function(v) { return /^(0x)?[0-9a-fA-F]{1,2}$/.test(v); } },
10
+ address: { value: '', required: true, validate: function(v) { const n = parseInt(v); return !isNaN(n) && n >= 1 && n <= 254; } },
11
11
  },
12
12
  inputs: 1,
13
13
  outputs: 2,
@@ -14,7 +14,7 @@
14
14
  defaults: {
15
15
  name: { value: '' },
16
16
  bridge: { value: '', type: 'velbus-bridge', required: true },
17
- address: { value: '', required: true, validate: function(v) { return /^(0x)?[0-9a-fA-F]{1,2}$/.test(v); } },
17
+ address: { value: '', required: true, validate: function(v) { const n = parseInt(v); return !isNaN(n) && n >= 1 && n <= 254; } },
18
18
  moduleName: { value: '' },
19
19
  typeId: { value: '' },
20
20
  },
@@ -12,7 +12,7 @@
12
12
  defaults: {
13
13
  name: { value: '' },
14
14
  bridge: { value: '', type: 'velbus-bridge', required: true },
15
- address: { value: '', required: true, validate: function(v) { return /^(0x)?[0-9a-fA-F]{1,2}$/.test(v); } },
15
+ address: { value: '', required: true, validate: function(v) { const n = parseInt(v); return !isNaN(n) && n >= 1 && n <= 254; } },
16
16
  moduleName: { value: '' },
17
17
  typeId: { value: '' },
18
18
  },
@@ -12,7 +12,7 @@
12
12
  defaults: {
13
13
  name: { value: '' },
14
14
  bridge: { value: '', type: 'velbus-bridge', required: true },
15
- address: { value: '', required: true, validate: function(v) { return /^(0x)?[0-9a-fA-F]{1,2}$/.test(v); } },
15
+ address: { value: '', required: true, validate: function(v) { const n = parseInt(v); return !isNaN(n) && n >= 1 && n <= 254; } },
16
16
  moduleName: { value: '' },
17
17
  typeId: { value: '' },
18
18
  },
@@ -11,7 +11,7 @@
11
11
  defaults: {
12
12
  name: { value: '' },
13
13
  bridge: { value: '', type: 'velbus-bridge', required: true },
14
- address: { value: '', required: true, validate: function(v) { return /^(0x)?[0-9a-fA-F]{1,2}$/.test(v); } },
14
+ address: { value: '', required: true, validate: function(v) { const n = parseInt(v); return !isNaN(n) && n >= 1 && n <= 254; } },
15
15
  moduleName: { value: '' },
16
16
  typeId: { value: '' },
17
17
  },
@@ -7,7 +7,7 @@
7
7
  defaults: {
8
8
  name: { value: '' },
9
9
  bridge: { value: '', type: 'velbus-bridge', required: true },
10
- address: { value: '', required: true, validate: function(v) { return /^(0x)?[0-9a-fA-F]{1,2}$/.test(v); } },
10
+ address: { value: '', required: true, validate: function(v) { const n = parseInt(v); return !isNaN(n) && n >= 1 && n <= 254; } },
11
11
  moduleName: { value: '' },
12
12
  },
13
13
  inputs: 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-velbus-2026",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Node-RED palette for Velbus building automation \u2014 relay, dimmer, glass panel, PIR, blind, sensor, meteo, clock nodes. Original and V2 (-20) series.",
5
5
  "keywords": [
6
6
  "node-red",
Binary file