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

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,79 @@ All feedback via GitHub issues, with examples and debug captures where possible.
14
14
 
15
15
  ---
16
16
 
17
+ ## v0.9.2 — 09/07/2026
18
+
19
+ ### velbus-glass-panel — VMBGPOD (0x28) registry gap fixed
20
+
21
+ - **Found by Stuart:** scanning his own home installation showed `unknown_0x28`
22
+ for three real modules — the palette had never included this type at all.
23
+ - **Real-world significance:** per the VLP training dataset analysed earlier
24
+ in this project, VMBGPOD is one of the **most common** glass panel types
25
+ in the field (978 occurrences, second only to VMB4RYLD and VMBDMI-R) — a
26
+ genuine, consequential gap, not an edge case. Only its V2 sibling
27
+ (`VMBGPOD-2`, 0x3D) had ever been added.
28
+ - Added to both `lib/glass-panel-types.js` (server-side) and the duplicate
29
+ type list in `velbus-glass-panel.html` (editor-side dropdown/display) —
30
+ confirmed both needed updating, not just one.
31
+ - **Confirmed from protocol_vmbgpod.pdf:** OLED display present; no
32
+ open-collector commands anywhere in the document (distinct from `VMBGPO`,
33
+ 0x21, a different product despite the similar name — that one does have
34
+ OC support).
35
+ - **One real mistake caught and corrected before shipping:** first pass
36
+ copied the channel count (32) from `VMBGPO` on the assumption it was the
37
+ closest sibling. Cross-checking the editor's own duplicate type list
38
+ before finalising showed `VMBGPOD-2` — the *actual* V2 sibling of this
39
+ same product line — lists 4 channels, not 32. Corrected to 4. Worth
40
+ remembering: when modelling a new registry entry on an existing one,
41
+ check for a same-name generational sibling before assuming the
42
+ closest-looking name is the right reference.
43
+ - Also identified in the same scan, **not yet added:** `VMBKP` (0x42,
44
+ "Keypad interface module") — a substantial 28-page protocol with its own
45
+ per-channel LED control layer, genuinely new territory rather than a
46
+ quick registry addition. Flagged for separate scoping, same approach as
47
+ velbus-energy before it was built.
48
+
49
+ ---
50
+
51
+ ## v0.9.1 — 08/07/2026
52
+
53
+ ### Address validation bug — 12 of 19 node types, any address ≥100 decimal
54
+
55
+ - **Reported by Stuart:** adding a velbus-sensor node and selecting VMB4AN
56
+ from the scan dropdown showed a red "not configured correctly" triangle
57
+ no matter what was selected.
58
+ - **Root cause:** every node using the `address` field (as opposed to
59
+ `moduleAddr`, used by relay/dimmer nodes, which were unaffected) validated
60
+ it against `/^(0x)?[0-9a-fA-F]{1,2}$/` — a pattern that only ever made
61
+ sense if `address` were stored as a 1-2 character hex string. But
62
+ `oneditsave` on every one of these nodes stores it as a plain decimal
63
+ **number** instead (`parseInt(...)`). Since regex `.test()` coerces its
64
+ argument to a string first, any address whose decimal representation is
65
+ 1-2 digits (1-99) happened to accidentally still match — every digit
66
+ 0-9 is also a valid hex character — masking the bug for low addresses.
67
+ Any address **100 or higher** (`0x64`+) produces a 3-digit decimal string,
68
+ which cannot match a `{1,2}`-length pattern, so validation always failed.
69
+ 100-254 is a very ordinary real-world address range — this was a live,
70
+ fully user-facing bug, not an edge case.
71
+ - **Affected nodes (12):** velbus-blind, velbus-blind-s, velbus-blind-20,
72
+ velbus-button, velbus-energy, velbus-glass-panel, velbus-meteo,
73
+ velbus-pir, velbus-pir-20, velbus-sensor, velbus-sensor-20,
74
+ velbus-thermostat. Confirmed relay/dimmer/relay-20/dimmer-20 (which use
75
+ `moduleAddr`, no custom validate function) and velbus-clock (no fixed
76
+ address at all) were never affected.
77
+ - **Fix:** replaced the regex with a direct numeric range check
78
+ (`parseInt(v)` between 1 and 254 inclusive) — correct regardless of
79
+ whether the stored value is a number, a decimal string, or a legacy hex
80
+ string (`parseInt` auto-detects a `0x` prefix), so old saved flows keep
81
+ working unchanged.
82
+ - Verified: syntax-checked the extracted JS from all 12 files, and
83
+ confirmed the corrected logic against the actual failing range (50/80/99
84
+ → true both before and after; 100/150/254 → **false before, true after**;
85
+ 0/255/empty/non-numeric → false, correctly rejected as invalid addresses
86
+ both before and after).
87
+
88
+ ---
89
+
17
90
  ## v0.9.0 — 08/07/2026
18
91
 
19
92
  ### velbus-energy — new node (19th node), VMBPSUMNGR-20
package/HANDOVER.md ADDED
@@ -0,0 +1,915 @@
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.2, 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 27 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 27 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, 27 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
+ | 0x28 | VMBGPOD | yes | no | no | — |
334
+ | 0x2D | VMBGP4PIR | no | yes | unconfirmed¹ | — |
335
+ | 0x34 | VMBEL1 | no | no | yes | 2 |
336
+ | 0x35 | VMBEL2 | no | no | yes | 2 |
337
+ | 0x36 | VMBEL4 | no | no | yes | 2 |
338
+ | 0x37 | VMBELO | yes | no | yes | 4 |
339
+ | 0x38 | VMBELPIR | no | yes | yes | 2 |
340
+ | 0x3A | VMBGP1-2 | no | no | no² | — |
341
+ | 0x3B | VMBGP2-2 | no | no | no² | — |
342
+ | 0x3C | VMBGP4-2 | no | no | no² | — |
343
+ | 0x3D | VMBGPOD-2 | yes | no | unconfirmed¹ | 2 |
344
+ | 0x47 | VMBEL2PIR | no | yes | yes | — |
345
+ | 0x4F | VMBEL1-20 | no | no | yes | — |
346
+ | 0x50 | VMBEL2-20 | no | no | yes | — |
347
+ | 0x51 | VMBEL4-20 | no | no | yes | — |
348
+ | 0x52 | VMBELO-20 | yes | no | yes | — |
349
+ | 0x53 | VMBELPIR-20³ | no | yes | yes | — |
350
+ | 0x54 | VMBGP1-20 | no | no | unconfirmed¹ | — |
351
+ | 0x55 | VMBGP2-20 | no | no | unconfirmed¹ | — |
352
+ | 0x56 | VMBGP4-20 | no | no | unconfirmed¹ | — |
353
+ | 0x57 | VMBGPO-20 | yes | no | yes | — |
354
+ | 0x5C | VMBEL2PIR-20³ | no | yes | yes | — |
355
+ | 0x5F | VMBGP4PIR-20 | no | yes | unconfirmed¹ | — |
356
+
357
+ ¹ Open-collector support unconfirmed against real hardware — see [section 13](#13-known-open-issues).
358
+ ² Confirmed no open-collector commands in the protocol PDF, but not yet live-verified.
359
+ ³ The official Velbus type list shows different module names at these two type bytes than
360
+ what this registry currently uses — flagged for verification, see
361
+ [section 13](#13-known-open-issues).
362
+
363
+ ### PIR sensors
364
+ | Type byte | Module | Node | Has temp. sensor |
365
+ |---|---|---|---|
366
+ | 0x23 | VMBPIRO-10 | velbus-pir | yes |
367
+ | 0x2A | VMBPIRM | velbus-pir | no |
368
+ | 0x2B | VMBPIRC | velbus-pir | no |
369
+ | 0x2C | VMBPIRO | velbus-pir | yes |
370
+ | 0x4D | VMBPIR-20 | velbus-pir-20 | no |
371
+ | 0x59 | VMBPIRO-20 | velbus-pir-20 | yes |
372
+
373
+ ### Sensors / meteo
374
+ | Type byte | Module | Node |
375
+ |---|---|---|
376
+ | 0x22 | VMB7IN | velbus-sensor |
377
+ | 0x31 | VMBMETEO | velbus-meteo |
378
+ | 0x32 | VMB4AN | velbus-sensor |
379
+ | 0x4E | VMB8IN-20 | velbus-sensor-20 |
380
+
381
+ ### Blind / shutter
382
+ | Type byte | Module | Node |
383
+ |---|---|---|
384
+ | 0x03 | VMB1BL | velbus-blind |
385
+ | 0x09 | VMB2BL | velbus-blind |
386
+ | 0x1D | VMB2BLE | velbus-blind-s |
387
+ | 0x2E | VMB1BLS | velbus-blind-s |
388
+ | 0x4A | VMB2BLE-10 | velbus-blind-s |
389
+ | 0x61 | VMB2BLE-20 | velbus-blind-20 |
390
+
391
+ ### Energy / infrastructure
392
+ | Type byte | Module | Node |
393
+ |---|---|---|
394
+ | 0x04 | VMBPSUMNGR-20 | velbus-energy |
395
+
396
+ ---
397
+
398
+ ## 7. Critical protocol knowledge
399
+
400
+ ### 7.1 Firmware build number — the official docs mislabel this field
401
+
402
+ In the `0xFF` module-identification response, bytes labelled "Build Year" and
403
+ "Build Week" in the official protocol PDFs are, in every module tested against real
404
+ hardware, actually the **high and low bytes of a single build number**, not a
405
+ year/week pair:
406
+
407
+ ```
408
+ build = (body[5] × 100) + body[6]
409
+ Example: body[5]=0x24 (36 decimal), body[6]=0x36 (54 decimal) → (36×100)+54 = 3654
410
+ ```
411
+
412
+ ### 7.2 `0xFF` module type/identification response
413
+ ```
414
+ body[0] = 0xFF
415
+ body[1] = module type byte
416
+ body[2] = serial number, high byte
417
+ body[3] = serial number, low byte
418
+ body[4] = memory map version
419
+ body[5] = build number high byte (decimal, see 7.1)
420
+ body[6] = build number low byte (decimal, see 7.1)
421
+ body[7] = properties byte — V2 modules only (bit 5 = CAN FD capable, bit 0 = terminator fitted)
422
+ ```
423
+ Original-series modules: 7 bytes total (no `body[7]`).
424
+ Exceptions confirmed against real hardware: **VMB1BL/VMB2BL** send only 5 bytes (no
425
+ serial number; `body[2]` is instead a DIP-switch timeout setting). **VMBELO**, despite
426
+ being an original-series module, sends the full 8-byte V2-style response.
427
+ **VMB2BLE-10** also sends 8 bytes, but `body[7]` there is only a terminator flag, not
428
+ the full V2 properties byte.
429
+
430
+ ### 7.3 `0xB0` subaddress response
431
+ ```
432
+ body[0] = 0xB0
433
+ body[1] = module type
434
+ body[2-3] = serial number
435
+ body[4-7] = up to four subaddresses (0xFF = subaddress slot disabled/unused)
436
+ ```
437
+ Subaddresses are **source addresses for incoming events only** — outgoing commands
438
+ always go to the module's primary address, never to a subaddress. The bridge handles
439
+ `0xB0` responses passively, building a subaddress→primary-address map so that other
440
+ nodes don't need any subaddress-specific logic themselves. A `0xB0` response can arrive
441
+ up to ~500ms after the initial `0xFF` — always allow for this delay before treating a
442
+ module's discovery as complete.
443
+
444
+ ### 7.4 `0xFB` relay status — two completely different formats depending on generation
445
+
446
+ **Original series** — one packet per active channel:
447
+ ```
448
+ body[0] = 0xFB
449
+ body[1] = channel number
450
+ body[2] = state (0 = off, 1 = on)
451
+ body[3-5] = 24-bit timer remaining, in seconds
452
+ body[6] = LED state
453
+ ```
454
+ Since one packet is sent per channel, allow up to ~200ms to collect all of them for a
455
+ multi-channel module.
456
+
457
+ **V2 series** — a single bitmask packet covering every channel at once:
458
+ ```
459
+ body[0] = 0xFB
460
+ body[1] = active (on) bitmask
461
+ body[2] = timer-running bitmask
462
+ body[3] = forced bitmask
463
+ body[4] = inhibited bitmask
464
+ body[5] = locked bitmask
465
+ ```
466
+
467
+ ### 7.5 `0xEE` dimmer status — V2 series (all three -20 dimmer types)
468
+ ```
469
+ body[0] = 0xEE
470
+ body[1] = on/off bitmask
471
+ body[2] = inhibited bitmask
472
+ body[3] = forced-on bitmask
473
+ body[4] = forced-off bitmask
474
+ body[5] = program-disabled bitmask
475
+ body[6] = error bitmask
476
+ body[7] = alarm/program byte
477
+ ```
478
+
479
+ ### 7.6 `0xA5` dim level — up to four channels packed per packet
480
+ ```
481
+ body[0] = 0xA5, body[1] = channel, body[2] = level (0-254)
482
+ [optionally repeated: body[3] = channel, body[4] = level, ...]
483
+ ```
484
+
485
+ ### 7.7 `0x1E` RGBW command — VMB4LEDPWM-20 in rgb/rgbw grouping mode only
486
+ ```
487
+ body[0] = 0x1E
488
+ body[1] = group (0xFF = all)
489
+ body[2] = R, body[3] = G, body[4] = B, body[5] = W
490
+ body[6] = fade mode (0 = direct, 1 = rate, 2 = time)
491
+ ```
492
+
493
+ **Watch this one carefully:** command bytes `0x12`/`0x14` mean **forced OFF / forced ON**
494
+ on relay nodes, but mean **forced UP / forced DOWN** on blind nodes. The same byte value
495
+ means something entirely different depending on module type — always check module type
496
+ before reusing a command byte pattern from a different node family.
497
+
498
+ ### 7.8 Memory read/write timing constraints
499
+ ```
500
+ Original series: minimum 20ms between 0xFC writes — this is real EEPROM with a
501
+ finite write-cycle lifespan, do not write faster than this
502
+ V2 series: wait for the module's 0xFE acknowledgement before the next write
503
+ OLED framebuffer: 10ms minimum per 4-byte block (V2 modules with a display)
504
+ Address assignment: allow 500ms, then rescan to confirm the new address took effect
505
+ ```
506
+ **Never write in response to a real-time bus event** — an incoming status broadcast is
507
+ not the right trigger for an automatic write-back, both for the EEPROM-wear reason above
508
+ and because it risks a feedback loop. Reads have no such constraint and are always safe.
509
+
510
+ ### 7.9 Thermostat commands — always to the primary address
511
+ ```
512
+ 0xDB = comfort mode, 0xDC = day mode, 0xDD = night mode, 0xDE = safe mode
513
+ 0xE0 = heat mode, 0xDF = cool mode
514
+ 0xE4 = set target temperature, 0xE5 = request temperature settings, 0xE7 = request status
515
+ 0xEF = request module name (append a channel byte)
516
+ 0xFA = request module status
517
+ ```
518
+ Thermostat commands go to the module's **primary address only**, never to a thermostat
519
+ subaddress, even though the thermostat function itself may be logically associated with
520
+ a subaddress in the module's own internal architecture.
521
+
522
+ ### 7.10 Temperature encoding — two different formats in use
523
+
524
+ ```javascript
525
+ // Signed byte × 0.5°C — used in the 0xE8 thermostat settings packet
526
+ function signed05(b) { return (b > 127 ? b - 256 : b) * 0.5; }
527
+
528
+ // Signed 16-bit, divided by 512 — used in the 0xE6 temperature packet
529
+ // (0.0625°C resolution). NOTE: official protocol documentation shows a
530
+ // divisor of 16 for this field — that is wrong. It has been directly
531
+ // verified against real hardware that the correct divisor is 512.
532
+ function tempFrom16(hi, lo) {
533
+ const raw = (hi << 8) | lo;
534
+ return (raw > 32767 ? raw - 65536 : raw) / 512;
535
+ }
536
+ ```
537
+ This divisor discrepancy (16 vs. 512) is exactly the kind of thing worth re-verifying
538
+ against a real module if you're implementing a new node that touches `0xE6` — don't
539
+ trust the official PDF's stated divisor for this specific field without checking.
540
+
541
+ ---
542
+
543
+ ## 8. Payload conventions
544
+
545
+ Every node's Node-RED output message follows the same conventions, regardless of module
546
+ type:
547
+
548
+ - **Numbers are always numbers, never strings.** A temperature is `21.5`, not `"21.5"`.
549
+ - **An `on` boolean is present only where a meaningful binary on/off state actually
550
+ exists.** Temperature, counter, and settings-only payloads have no `on` field at all —
551
+ don't add one just for consistency's sake if the underlying value isn't truly binary.
552
+ - **`topic`** identifies the payload shape/purpose (e.g. `"relay_status"`,
553
+ `"dimmer_status"`, `"button"`, `"thermostat_status"`) so a single debug/switch node
554
+ downstream can distinguish message types without inspecting every field.
555
+
556
+ Representative examples:
557
+
558
+ ```json
559
+ { "topic": "relay_status", "address": "0x10", "module": "Hall Relay",
560
+ "channel": 1, "state": "on", "on": true, "timerRemaining": 0 }
561
+
562
+ { "topic": "dimmer_status", "address": "0xA5", "module": "GF Lights",
563
+ "channel": 1, "state": "on", "on": true, "level": 187, "percent": 73.6,
564
+ "outputType": "PWM", "dimCurve": "exponential", "ledMode": "single" }
565
+
566
+ { "topic": "button", "address": "0x03", "module": "Hall Panel",
567
+ "type": "button", "on": true,
568
+ "pressed": [1, 3], "released": [], "longPressed": [] }
569
+
570
+ { "topic": "thermostat_status", "type": "thermostat",
571
+ "currentTemp": 21.5, "targetTemp": 22.0, "mode": "comfort",
572
+ "heaterMode": true, "heating": false, "thermostatOn": true }
573
+
574
+ { "topic": "temperature", "type": "temperature",
575
+ "current": 21.5, "min": 15.0, "max": 30.0 }
576
+
577
+ { "topic": "meteo", "type": "meteo",
578
+ "rain": 0.5, "light": 12500, "wind": 14.3 }
579
+
580
+ { "topic": "blind_status", "type": "blind_status",
581
+ "channel": 1, "on": true, "status": "up", "position": 25,
582
+ "lockState": "normal", "autoMode": 0 }
583
+
584
+ { "topic": "module_online", "address": "0x06", "module": "VMB4LEDPWM-20",
585
+ "typeId": "0x06", "outputType": "PWM", "dimCurve": "exponential",
586
+ "ledMode": "single", "channels": 4, "build": 2436, "canFD": false }
587
+ ```
588
+
589
+ **Node status bar format** (shown under each node in the Node-RED editor): `"{module
590
+ name} (0x{address}) {state}"`. Name priority, highest first: an explicit user override
591
+ in node config, then the module's own name as reported by VelbusLink/read from the
592
+ module itself, then falling back to the generic module type string if neither is
593
+ available.
594
+
595
+ ---
596
+
597
+ ## 9. Architecture decisions (settled — do not reopen casually)
598
+
599
+ These are decisions made deliberately, generally after real back-and-forth about the
600
+ alternatives. Reopening one isn't forbidden, but should be a considered choice, not an
601
+ accidental drift — if you find yourself changing one of these, it's worth being explicit
602
+ about why in the commit message.
603
+
604
+ - **Generational split.** Nodes generally split at the V2.0 boundary — one node for
605
+ original-series, a separate node for V2. **Exceptions:** `velbus-glass-panel` (one
606
+ node covers all 27 types, both generations, via the type registry's per-type flags
607
+ rather than a code-level split), `velbus-thermostat` (covers the thermostat function
608
+ on any glass panel type), `velbus-meteo` (only one generation exists), and the blind
609
+ family (`velbus-blind` / `velbus-blind-s` / `velbus-blind-20`, split by actual protocol
610
+ capability rather than strictly by generation, since some non-V2 blind modules share
611
+ more protocol in common with certain V2 ones than with other original-series ones).
612
+ - **Firmware check (V2 nodes only).** A three-stage check on receiving the module's
613
+ `0xFF` identification: type byte matches → memory map version meets the node's stated
614
+ minimum → pass. Failure is a hard block with a red status, not a soft warning.
615
+ Original-series nodes skip this check entirely (no firmware/map-version concept
616
+ exists for them).
617
+ - **Name auto-retrieval.** V2 nodes, and the glass-panel/thermostat nodes, request the
618
+ module's name (`0xEF`) automatically on startup, with a 2-second timeout — whatever
619
+ has been received by then is used, even if the name is incomplete.
620
+ - **Scan lock.** `velbus-scan` calls the bridge's scan-lock before scanning; every other
621
+ node's own startup RTR requests are queued during an active scan and flushed at a rate
622
+ of one per second once scanning completes, rather than being sent immediately and
623
+ risking a collision with in-progress scan traffic.
624
+ - **Address dropdown data source.** Always read `results.modules`, never `results`
625
+ directly, from the bridge's `/velbus/scan-results` endpoint — it returns an object
626
+ wrapping the array, not the array itself. Real, repeated source of bugs — worth
627
+ restating even though it's also in section 4.1.
628
+ - **Thermostat commands always target the primary address**, never a subaddress, even
629
+ though the thermostat function may be logically tied to one internally.
630
+ - **`0xFF` response body length** is 7 bytes on original-series modules, 8 bytes on V2
631
+ — except VMBELO (original series, but sends 8 bytes) and VMB2BLE-10 (sends 8 bytes,
632
+ but the 8th is only a terminator flag, not full V2 properties). See section 7.2.
633
+ - **VMB4LEDPWM-20 LED grouping mode** is a node config property that documents which
634
+ mode (single/rgb/rgbw) the physical module is wired for — the node does **not** write
635
+ this setting to the module automatically. Changing a module's grouping mode is a
636
+ deliberate commissioning-time decision made by a human (or a dedicated commissioning
637
+ tool), not something a live command node should do silently. A read-only
638
+ `get_device_type` command exists on `velbus-dimmer-20` to verify the module's actual
639
+ setting matches the node's configured value.
640
+ - **`velbus-clock`'s `set_alarm` command accepts a per-message address override**,
641
+ breaking from the "one node = one fixed module address" pattern every other node
642
+ follows. This was a deliberate exception, not an oversight: the underlying protocol
643
+ command (`0xC3`, clock alarm) is identical whether sent globally (broadcast) or
644
+ locally (one specific module) — differing only in destination address — and is a
645
+ shared system-level command across V2 module types generally, not a feature tied to
646
+ one specific module type. Putting per-module alarm handling on every relevant node
647
+ type instead would mean duplicating an identical handler several times over. This is
648
+ flagged as a considered judgement call rather than a permanently settled decision —
649
+ if it later seems better as a proper per-module config field (matching every other
650
+ node's pattern), that's a reasonable direction to switch to.
651
+
652
+ ---
653
+
654
+ ## 10. Development environment setup
655
+
656
+ **Prerequisites:**
657
+ - Node.js ≥ 14
658
+ - Node-RED ≥ 2.0.0
659
+ - A Velbus TCP gateway of some kind — either the official `velbus-tcp` package, or the
660
+ open-source `python-velbustcp` project (see [section 15](#15-where-to-find-protocol-references)
661
+ for links). Both simply expose the same bus protocol over a TCP socket; this palette
662
+ doesn't care which one it's talking to.
663
+ - Ideally, physical Velbus hardware to test against — though a great deal of useful
664
+ development and verification is possible without it; see
665
+ [section 11](#11-testing-without-real-hardware--the-mock-harness-pattern).
666
+
667
+ **For live development** (editing node code and seeing changes without repackaging a
668
+ tarball every time), symlink this repository directly into Node-RED's node_modules
669
+ rather than repeatedly installing from a tarball:
670
+ ```bash
671
+ cd ~/.node-red/node_modules
672
+ ln -s /path/to/your/local/checkout/node-red-contrib-velbus-2026 node-red-contrib-velbus-2026
673
+ ```
674
+ Restart Node-RED after each code change to pick it up (Node-RED does not hot-reload
675
+ node `.js` files). For `.html` (editor UI/help) changes specifically, a hard browser
676
+ refresh (`Ctrl+Shift+R` or equivalent) is usually enough, since the editor caches node
677
+ HTML client-side — a full Node-RED restart isn't always necessary for HTML-only changes,
678
+ but doesn't hurt if something isn't picking up.
679
+
680
+ **No physical Velbus hardware and no gateway software set up yet?** You can still make
681
+ substantial progress — see the next section.
682
+
683
+ ---
684
+
685
+ ## 11. Testing without real hardware — the mock-harness pattern
686
+
687
+ A useful, repeatedly-proven technique for verifying a node's packet-building logic
688
+ *before* ever touching real hardware: write a small standalone script that mocks just
689
+ enough of the Node-RED `RED` object to load the node file directly, wire a fake bridge
690
+ that captures whatever bytes get sent, and hand-verify the resulting packet against the
691
+ protocol specification (checksum, DLC, byte-for-byte field layout).
692
+
693
+ This does **not** replace testing against real hardware — it catches packet-construction
694
+ bugs (wrong byte count, wrong checksum, off-by-one field indexing) before they ever reach
695
+ a bus, but it cannot confirm that a real module actually does what the protocol PDF says
696
+ it should do in response. Both kinds of verification matter; this one is simply always
697
+ available, with zero hardware dependency.
698
+
699
+ **Minimal example** (adapt the node path, config, and input payload to whatever you're
700
+ testing):
701
+
702
+ ```javascript
703
+ const handlers = {};
704
+
705
+ function MockNode() {
706
+ this.on = (evt, fn) => { handlers[evt] = fn; };
707
+ this.status = (s) => console.log('STATUS:', JSON.stringify(s));
708
+ this.warn = (m) => console.log('WARN:', m);
709
+ this.error = (m) => console.log('ERROR:', m);
710
+ this.send = (msg) => console.log('OUTPUT:', JSON.stringify(msg));
711
+ }
712
+
713
+ const sentPackets = [];
714
+ const mockBridge = {
715
+ isConnected: () => true,
716
+ send: (buf) => sentPackets.push(buf)
717
+ };
718
+
719
+ const RED = {
720
+ nodes: {
721
+ createNode: (self) => { Object.assign(self, new MockNode()); },
722
+ getNode: () => mockBridge,
723
+ registerType: (name, ctor) => { RED._ctor = ctor; }
724
+ }
725
+ };
726
+
727
+ require('/path/to/nodes/velbus-example/velbus-example.js')(RED);
728
+
729
+ const config = { bridge: 'x' /* , ...whatever config fields this node expects */ };
730
+ new RED._ctor(config);
731
+
732
+ // Fire whatever input message you want to test
733
+ handlers['input']({ payload: { cmd: 'some_command', /* ...fields */ } });
734
+
735
+ // Inspect the resulting bytes
736
+ for (const buf of sentPackets) {
737
+ console.log(buf.toString('hex').toUpperCase().match(/.{1,2}/g).join(' '));
738
+ }
739
+ ```
740
+
741
+ Hand-verify the printed hex bytes against the protocol PDF: confirm the DLC nibble
742
+ matches the actual body length, confirm each field lands at the expected offset, and
743
+ compute the checksum by hand (sum all bytes before it, mod 256, two's complement) to
744
+ confirm it matches what the code produced. This exact pattern has caught real bugs
745
+ before code ever reached a physical bus — worth running for any new packet-building
746
+ logic, not just as an afterthought.
747
+
748
+ ---
749
+
750
+ ## 12. Testing status — what's actually verified
751
+
752
+ Testing maturity **varies significantly by node** — do not assume something works
753
+ against real hardware just because it's present in the published package. The
754
+ authoritative, version-by-version record is `CHANGELOG_FORUM.md`; check the entries for
755
+ the specific node/command you're relying on before treating it as proven. As a general
756
+ orientation at the time of writing:
757
+
758
+ - **Field-tested against live hardware:** the core relay, dimmer, glass panel, and
759
+ thermostat nodes have real-hardware confirmation for their primary functions.
760
+ - **Mock-harness verified only, not yet confirmed against a real bus:** `velbus-clock`
761
+ (both the time/date/DST broadcast and the `set_alarm` command), the
762
+ `velbus-dimmer-20` `get_device_type` read command, and `velbus-energy` in its
763
+ entirety (no VMBPSUMNGR-20 has been confirmed present on a scanned bus yet).
764
+ - **Fixed but pending re-confirmation on hardware different from what it was originally
765
+ tested on:** several nodes had packet-construction bugs fixed in bulk during one
766
+ intensive debugging pass (see `CHANGELOG_FORUM.md`, the entries covering versions
767
+ 0.6.6 through 0.7.0) — confirm against whatever specific module you're working with
768
+ if you haven't personally seen it work.
769
+
770
+ ---
771
+
772
+ ## 13. Known open issues
773
+
774
+ - **`VMBKP` (0x42, "Keypad interface module") has no node at all.** Found scanning a
775
+ real installation (Stuart's home) alongside the `VMBGPOD` gap below — a genuinely new
776
+ module type, not yet scoped. Its protocol PDF (`protocol_vmbkp.pdf`, 28 pages) is
777
+ substantial: channel status, module status, and a full per-channel LED control layer
778
+ (clear/set/slow-blink/fast-blink/very-fast-blink), similar in spirit to `velbus-button`
779
+ but with LED feedback control `velbus-button` doesn't have. This needs the same
780
+ "how much work would this involve" scoping pass `velbus-energy` got before it was
781
+ built, not a quick bolt-on to an existing node.
782
+ - **Open-collector support** on several glass panel types (marked "unconfirmed" in the
783
+ registry in section 6) has not been verified against real hardware — the protocol PDFs
784
+ are ambiguous or silent on some of these. Sending `0x01`/`0x02`/`0x03` open-collector
785
+ commands to one of these modules and observing whether it responds would resolve this.
786
+ - **Module names at type bytes `0x53` and `0x5C`** — the official Velbus type list shows
787
+ different module names than what this registry currently uses for these two type
788
+ bytes. Needs verification against a real module or a VelbusLink bus scan.
789
+ - **`velbus-energy` verified only against a mock test harness** — no
790
+ VMBPSUMNGR-20 has actually been confirmed present on a scanned bus yet, only
791
+ assumed likely present at a known installation. Packet checksums and field
792
+ layout are hand-verified against the protocol PDF; real-hardware behaviour
793
+ is unconfirmed. The `0xA3` (PSU values) byte layout in particular was
794
+ reconstructed from an internally-inconsistent section of the official PDF
795
+ (see `CHANGELOG_FORUM.md`, v0.9.0) rather than read directly — the single
796
+ most likely spot for a real decode error to surface once tested live.
797
+ - **V2 relay interval/blink timer** — confirmed there is no live bus command for this on
798
+ V2-series relays at all (unlike the original series, which has one). A real interval
799
+ timer on a V2 relay requires writing a Program Step to the module's memory (protocol
800
+ command `0xC2`, Action code 22), which is a fundamentally different, more involved
801
+ kind of operation than any other command node in this palette currently performs.
802
+ Whether this belongs in this palette at all (as a new, more involved node) or is out
803
+ of scope is an open question, not a settled one.
804
+ - **`velbus-clock`'s multi-channel behaviour** during simultaneous multi-channel relay
805
+ blinking (interval timer) hasn't been explicitly tested for whether `channel`/
806
+ `channelBit` fields report correctly when more than one channel blinks at once.
807
+ - **DST auto-detection heuristic** in `velbus-clock` (comparing the current UTC offset
808
+ against the year's January/July offsets to infer whether daylight saving is active)
809
+ has only been sanity-checked in an environment where daylight saving never applies —
810
+ a genuine positive "DST is active" case hasn't been observed and confirmed correct.
811
+
812
+ ---
813
+
814
+ ## 14. Contribution and release workflow
815
+
816
+ **Every release, regardless of size:**
817
+
818
+ 1. **Bump the version** in `package.json` — npm refuses to publish an already-used
819
+ version number, even for a trivial fix. Add a matching entry to
820
+ `CHANGELOG_FORUM.md` describing what changed and, where relevant, *why* — the reasoning
821
+ behind a fix is often more valuable to a future reader than the diff itself.
822
+ 2. **Commit and push:**
823
+ ```bash
824
+ git add -A
825
+ git commit -m "Describe the change"
826
+ git push
827
+ ```
828
+ 3. **Publish to npm:**
829
+ ```bash
830
+ npm publish
831
+ ```
832
+
833
+ **Publishing requires either 2FA enabled on the npm account used, or a Granular Access
834
+ Token with "Bypass two-factor authentication" explicitly ticked** — npm's current
835
+ policy rejects publish attempts otherwise with a `403` error. Whoever is publishing
836
+ needs one or the other set up; this has been a real stumbling block in practice.
837
+
838
+ **Recommended, not required:** tag the release to match, so it's easy to see which
839
+ commit corresponds to which published version:
840
+ ```bash
841
+ git tag v<version>
842
+ git push --tags
843
+ ```
844
+
845
+ **When adding a new node:**
846
+ - Copy the closest existing equivalent as a starting structure rather than writing from
847
+ scratch — the bridge lookup, packet registration, and status-bar boilerplate is
848
+ consistent throughout and easy to get subtly wrong if reinvented.
849
+ - Register it in `package.json`'s `node-red.nodes` map — Node-RED will not discover a
850
+ node that exists as a file but isn't listed there.
851
+ - Verify the packet construction using the mock-harness pattern
852
+ ([section 11](#11-testing-without-real-hardware--the-mock-harness-pattern)) before
853
+ ever sending it to a real bus.
854
+ - Add its testing status honestly to `CHANGELOG_FORUM.md` — "verified via mock harness
855
+ only, not yet confirmed on real hardware" is a completely acceptable and expected
856
+ status for a new addition; the goal is accuracy, not appearing more finished than it is.
857
+
858
+ ---
859
+
860
+ ## 15. Where to find protocol references
861
+
862
+ - **`https://github.com/velbus/packetprotocol`** — the low-level packet framing
863
+ specification (start/end markers, priority byte values, checksum algorithm).
864
+ - **`https://github.com/velbus/moduleprotocol`** — per-module protocol PDFs, one (or a
865
+ few, where several similar modules share one document) per module type. This is the
866
+ authoritative source for any module-specific command/status byte layout — always
867
+ check the actual PDF for the specific module type you're working on rather than
868
+ assuming it matches a similar-looking one, given how many small but real differences
869
+ exist between superficially similar modules (see sections 7.2 and 7.10 for two
870
+ concrete examples of official documentation being subtly wrong or inconsistent).
871
+ - **`https://github.com/velbus/python-velbustcp`** — an open-source TCP gateway
872
+ implementation; one of two practical ways (alongside Velbus's own `velbus-tcp`
873
+ package) to get a real bus accessible over TCP for this palette to connect to.
874
+ - The Velbus community forum (search for "building custom velbus devices") has at least
875
+ one publicly documented from-scratch virtual-module implementation, which has proven a
876
+ useful cross-reference for less-obvious bit-level encoding questions (particularly
877
+ around relay status packet formats) beyond what the official PDFs spell out clearly.
878
+ - Other open-source Velbus integrations worth cross-referencing when a protocol question
879
+ isn't fully resolved by the official docs: the openHAB Velbus binding, and the
880
+ HomeAssistant `velbusaio` integration — both are mature, independently-implemented
881
+ interpretations of the same protocol, and occasionally clarify an ambiguity the
882
+ official PDFs leave unclear.
883
+
884
+ ---
885
+
886
+ ## 16. Code style rules
887
+
888
+ - **No pseudocode, ever.** Every function, file, and HTML block committed should be a
889
+ complete, working, drop-in replacement — never a partial sketch or a "fill this in"
890
+ skeleton.
891
+ - **Prove the logic before restructuring it.** Get a single packet working and verified
892
+ (even just via the mock harness) before refactoring for elegance or reuse.
893
+ - **`body[0]` is always the command byte.** Real data starts at `body[1]`. No
894
+ exceptions — see section 4.3 if this isn't already second nature.
895
+ - **Numbers are always numbers in payloads, never strings.**
896
+ - **`results.modules`, not `results`,** when reading the scan endpoint.
897
+ - **Thermostat commands go to the primary address only,** never a subaddress.
898
+ - **Respect the 20ms minimum between `0xFC` writes** on original-series modules — this
899
+ is real EEPROM wear, not an arbitrary rate limit.
900
+ - **British English** throughout documentation and in-editor help text (this is a
901
+ British-developed project for what is predominantly a European/UK installer base —
902
+ purely a style convention, not a functional requirement).
903
+
904
+ ---
905
+
906
+ ## 17. License and attribution
907
+
908
+ MIT licensed — see `LICENSE`.
909
+
910
+ Originally developed by Stuart Hanlon / MDAR Limited, the UK Velbus distributor
911
+ (`mdar.co.uk`), as a modern replacement for an earlier, unmaintained community Velbus
912
+ Node-RED palette. Protocol reverse-engineering and verification work throughout this
913
+ project has drawn on the official Velbus protocol repositories, the Velbus community
914
+ forum, and cross-referencing against other independent open-source Velbus integrations
915
+ — 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
  ---
@@ -61,6 +61,30 @@ const GLASS_PANEL_TYPES = {
61
61
  minMapVer: 2,
62
62
  series: 'original',
63
63
  },
64
+ 0x28: {
65
+ name: 'VMBGPOD',
66
+ // Added 09/07/2026 — found as "unknown_0x28" scanning a real installation.
67
+ // One of the most common glass panel types in the field (978 occurrences
68
+ // across the VLP training dataset analysed earlier in this project,
69
+ // second only to VMB4RYLD and VMBDMI-R) — a genuine gap, not an edge case.
70
+ // Source: protocol_vmbgpod.pdf (ed.6, 04/12/2024), "Touch panel with Oled
71
+ // display". Distinct from VMBGPO (0x21) despite similar naming/features —
72
+ // VMBGPOD's protocol PDF has no open-collector command at all (checked:
73
+ // no "collector" or "channel 18"-style pattern anywhere in the document),
74
+ // whereas VMBGPO's family confirms OC support. These are NOT the same
75
+ // hardware feature set despite the name similarity.
76
+ // channels: 4, matched against VMBGPOD-2 (0x3D) — the correct sibling,
77
+ // being the V2 version of this SAME product line. (First attempt at this
78
+ // entry wrongly copied VMBGPO's 32 — a different product that just
79
+ // happens to have a similar name. Caught by cross-checking the editor's
80
+ // own duplicate type list before finalising, not from the PDF directly.)
81
+ channels: 4,
82
+ hasOled: true,
83
+ hasPir: false,
84
+ hasOc: false, // confirmed: no OC commands anywhere in this PDF
85
+ minMapVer: null, // not found stated in this PDF
86
+ series: 'original',
87
+ },
64
88
  0x2D: {
65
89
  name: 'VMBGP4PIR',
66
90
  channels: 8,
@@ -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
  },
@@ -6,6 +6,7 @@
6
6
  0x1E: { name: 'VMBGP1', channels: 1, oled: false, pir: false, series: 'original' },
7
7
  0x1F: { name: 'VMBGP2', channels: 2, oled: false, pir: false, series: 'original' },
8
8
  0x21: { name: 'VMBGPO', channels: 32, oled: true, pir: false, series: 'original' },
9
+ 0x28: { name: 'VMBGPOD', channels: 4, oled: true, pir: false, series: 'original' },
9
10
  0x2D: { name: 'VMBGP4PIR', channels: 8, oled: false, pir: true, series: 'original' },
10
11
  0x34: { name: 'VMBEL1', channels: 1, oled: false, pir: false, series: 'original' },
11
12
  0x35: { name: 'VMBEL2', channels: 2, oled: false, pir: false, series: 'original' },
@@ -135,7 +136,7 @@
135
136
  defaults: {
136
137
  name: { value: '' },
137
138
  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); } },
139
+ address: { value: '', required: true, validate: function(v) { const n = parseInt(v); return !isNaN(n) && n >= 1 && n <= 254; } },
139
140
  channelCount: { value: 4, required: true, validate: RED.validators.number() },
140
141
  moduleName: { value: '' },
141
142
  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.2",
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