org.knx cannot control a KNX installation whose ETS project uses 2-level group
addresses (main/sub, e.g. 6/7).
Devices pair normally, the KNX/IP interface connects, the tunnel stays up and
sends keepalives, and the devices show as available. But no telegram is ever put
on the bus. There is no error in the app, no unavailable device, and nothing in
the UI indicating a problem.
A secondary symptom is a reconnect loop: CONNECT_REQUEST roughly every 3 seconds.
This is a consequence of the same defect, not a separate issue.
Root cause
The bundled knx library (knx@2.5.4, ekarak/knx) supports both addressing modes.
Address.parse() and Address.toString() select the mode from their third
parameter:
node_modules/knx/src/Address.js
const parse = function (addr, addrtype, twoLevelAddressing) {
...
if (group && twoLevelAddressing) {
address.writeUInt16BE((hinibble << 11) + midnibble, 0);
return address;
}
if (tokens.length < 3) throw 'Invalid address - missing 3rd token';
KnxProtocol.js calls both functions without that third argument, so it is always
undefined -> falsy -> 3-level, in both directions:
node_modules/knx/src/KnxProtocol.js (encode, in CEMI.write)
.raw(KnxAddress.parse(value.dest_addr, value.ctrl.destAddrType), 2);
node_modules/knx/src/KnxProtocol.js (decode, in the CEMI reader)
hdr.dest_addr = KnxAddress.toString(
hdr.dest_addr,
hdr.ctrl.destAddrType
);
A 2-token address therefore always reaches the 3-level branch and throws
'Invalid address - missing 3rd token'.
Two flags look like they control this, but both are set and never read:
knx/src/FSM.js:22 this.ThreeLevelGroupAddressing = true
knx/src/KnxProtocol.js:16 KnxProtocol.twoLevelAddressing = false
Why it fails silently
FSM.prototype.write() stores the group address as a string and hands the datagram
to the state machine; its callback fires immediately, so writeKNXGroupAddress()
resolves successfully and the device reports no error.
The string is only parsed later, at serialisation time inside sendDatagram. The
throw therefore happens asynchronously, outside the try/catch in write(), and is
never surfaced to the app or the user.
That same exception interrupts the sendDatagram transition, so the FSM falls back
to 'connecting', whose setInterval(..., 3000) re-sends CONNECT_REQUEST. That is
the reconnect loop.
Steps to reproduce
- Add a KNX device using a 2-level group address, e.g. ga_switch = 6/7.
- Pair it against any KNX/IP interface.
- Toggle the device.
Expected: a GroupValueWrite telegram to 0x3007 on the bus.
Actual: no TUNNELING_REQUEST is sent. A packet capture on port 3671 shows only
CONNECT_REQUEST / CONNECT_RESPONSE and CONNECTIONSTATE keepalives, no data frames.
Using the 3-level form of the same address (6/0/7) works immediately. Both encode
to the identical 16-bit value 0x3007, so this is purely an encoder-side defect.
Suggested fix
Three changes are sufficient.
-
knx/src/KnxProtocol.js - encode
- .raw(KnxAddress.parse(value.dest_addr, value.ctrl.destAddrType), 2);
- .raw(KnxAddress.parse(value.dest_addr, value.ctrl.destAddrType, KnxProtocol.twoLevelAddressing), 2);
-
knx/src/KnxProtocol.js - decode
hdr.dest_addr = KnxAddress.toString(
hdr.dest_addr,
-
-
KnxProtocol.twoLevelAddressing
);
Both directions must change together. Device settings store the group address
as a string and the app keys its bus listeners on that string, so decoding
inbound telegrams in the other format would break status feedback.
src_addr is a physical address and is unaffected: Address.parse only applies
twoLevelAddressing when addrtype === GROUP.
-
lib/KNXInterface.js - select the mode
const KnxProtocol = require('knx/src/KnxProtocol');
...
KnxProtocol.twoLevelAddressing = <true for 2-level projects>;
Ideally exposed as a user setting ("group address style: 2-level / 3-level"),
defaulting to 3-level.
The library default stays false, so the change is backward compatible and 3-level
installations are unaffected.
Verified behaviour with the above applied:
6/0 -> 0x3000 -> "6/0"
6/7 -> 0x3007 -> "6/7"
6/207 -> 0x30cf -> "6/207"
6/407 -> 0x3197 -> "6/407" (sub-group > 255)
13/100 -> 0x6864 -> "13/100"
2-level "6/7" and 3-level "6/0/7" both encode to 0x3007
3-level parse("6/7") still throws 'missing 3rd token' (unchanged)
Workaround
Re-express each 2-level address in its 3-level equivalent. The raw value is
identical, so the bus is unaffected:
raw = (main << 11) + sub
3-level = (raw >> 11) / ((raw >> 8) & 7) / (raw & 255)
6/7 -> 6/0/7 6/207 -> 6/0/207
6/407 -> 6/1/151 6/807 -> 6/3/39
Sub-groups above 255 do not map to main/0/sub; they must be converted through the
raw value.
Environment
org.knx 1.5.2 (SDK 3)
dependency knx ^2.5.4 (ekarak/knx)
Homey: Homey Pro (Early 2023), firmware 13.3.0
Interfaces: Weinzierl KNX IP Interface 731
knxd 0.14.73
org.knx cannot control a KNX installation whose ETS project uses 2-level group
addresses (main/sub, e.g. 6/7).
Devices pair normally, the KNX/IP interface connects, the tunnel stays up and
sends keepalives, and the devices show as available. But no telegram is ever put
on the bus. There is no error in the app, no unavailable device, and nothing in
the UI indicating a problem.
A secondary symptom is a reconnect loop: CONNECT_REQUEST roughly every 3 seconds.
This is a consequence of the same defect, not a separate issue.
Root cause
The bundled knx library (knx@2.5.4, ekarak/knx) supports both addressing modes.
Address.parse() and Address.toString() select the mode from their third
parameter:
node_modules/knx/src/Address.js
KnxProtocol.js calls both functions without that third argument, so it is always
undefined -> falsy -> 3-level, in both directions:
node_modules/knx/src/KnxProtocol.js (encode, in CEMI.write)
node_modules/knx/src/KnxProtocol.js (decode, in the CEMI reader)
A 2-token address therefore always reaches the 3-level branch and throws
'Invalid address - missing 3rd token'.
Two flags look like they control this, but both are set and never read:
Why it fails silently
FSM.prototype.write() stores the group address as a string and hands the datagram
to the state machine; its callback fires immediately, so writeKNXGroupAddress()
resolves successfully and the device reports no error.
The string is only parsed later, at serialisation time inside sendDatagram. The
throw therefore happens asynchronously, outside the try/catch in write(), and is
never surfaced to the app or the user.
That same exception interrupts the sendDatagram transition, so the FSM falls back
to 'connecting', whose setInterval(..., 3000) re-sends CONNECT_REQUEST. That is
the reconnect loop.
Steps to reproduce
Expected: a GroupValueWrite telegram to 0x3007 on the bus.
Actual: no TUNNELING_REQUEST is sent. A packet capture on port 3671 shows only
CONNECT_REQUEST / CONNECT_RESPONSE and CONNECTIONSTATE keepalives, no data frames.
Using the 3-level form of the same address (6/0/7) works immediately. Both encode
to the identical 16-bit value 0x3007, so this is purely an encoder-side defect.
Suggested fix
Three changes are sufficient.
knx/src/KnxProtocol.js - encode
knx/src/KnxProtocol.js - decode
Both directions must change together. Device settings store the group address
as a string and the app keys its bus listeners on that string, so decoding
inbound telegrams in the other format would break status feedback.
src_addr is a physical address and is unaffected: Address.parse only applies
twoLevelAddressing when addrtype === GROUP.
lib/KNXInterface.js - select the mode
const KnxProtocol = require('knx/src/KnxProtocol');
...
KnxProtocol.twoLevelAddressing = <true for 2-level projects>;
Ideally exposed as a user setting ("group address style: 2-level / 3-level"),
defaulting to 3-level.
The library default stays false, so the change is backward compatible and 3-level
installations are unaffected.
Verified behaviour with the above applied:
Workaround
Re-express each 2-level address in its 3-level equivalent. The raw value is
identical, so the bus is unaffected:
Sub-groups above 255 do not map to main/0/sub; they must be converted through the
raw value.
Environment