Skip to content

2-level KNX group addresses are silently unsupported - devices connect but never transmit #92

Description

@Tim0kot

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

  1. Add a KNX device using a 2-level group address, e.g. ga_switch = 6/7.
  2. Pair it against any KNX/IP interface.
  3. 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.

  1. 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);
  2. knx/src/KnxProtocol.js - decode

        hdr.dest_addr = KnxAddress.toString(
          hdr.dest_addr,
    
    •    hdr.ctrl.destAddrType
      
    •    hdr.ctrl.destAddrType,
      
    •    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.

  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions