Skip to content

Add support for the new ComfoConnect Pro#74

Open
jvanhees wants to merge 2 commits into
michaelarnauts:masterfrom
jvanhees:comfoconnect-pro-connection
Open

Add support for the new ComfoConnect Pro#74
jvanhees wants to merge 2 commits into
michaelarnauts:masterfrom
jvanhees:comfoconnect-pro-connection

Conversation

@jvanhees

Copy link
Copy Markdown

I installed a new ComfoConnect Pro a few weeks ago, which seems to be replacing the previous version, and adds Wifi and other functionality that was in another connect box previously. The library didn't seem to properly connect to this new ComfoConnect Pro, even though it does support the same protocol. After some retrying, the library is able to connect, but since it initially fails the HACS integration doesn't really play nice. I used Claude to change the connection logic to also support this device, apparently it keeps the TCP connection open instead of disconnecting after a failed authentication, and just waits for a new registration.

I asked Claude to also add some test for this functionality, all other tests are still working. I do note that whenever I run the discover functionality the first time, it tends to not find the bridge. After running it a second time, it does run. Might need some more investigation but I suspect this won't be an issue with the Home Assistant integration.

Please let me know if you need anything changed or something else. Hopefully we can merge this, update the HACS integration as well with the new version and get the ComfoConnect Pro working in Home Assistant :) .

@jvanhees

Copy link
Copy Markdown
Author

Related to this discussion: #55

@jvanhees jvanhees changed the title Add support for the new Comfoconnect Pro Add support for the new ComfoConnect Pro May 27, 2026
@michaelarnauts

Copy link
Copy Markdown
Owner

Thanks!

I think this looks fine, odd that there is such a small difference between the two devices.

Can you take a look at the lint issue?

@jvanhees

Copy link
Copy Markdown
Author

I agree it's odd that there is this difference between the two devices, but I suspect that they may have rewritten some of the networking inside the device, but were able to salvage much of the software of the old device? Or they just completely rebuild it using the existing spec, who knows. To be honest, I haven't tested the complete integration yet so I'm not sure if all the addresses are the same - but there's no reason for me to suspect they've changed.

Linting issue has been fixed.

@michaelarnauts

Copy link
Copy Markdown
Owner

Thanks! I do wonder if maybe the flow should be changed, because currently, we are waiting for a timeout to know that we should register. This can perhaps explain the timeout you are also seeing in Home Assistant.

If you don't have a better idea, I'm okay with merging this.

@michaelarnauts

Copy link
Copy Markdown
Owner

CI still fails. Can you also run black to format the code?

poetry run black --check aiocomfoconnect/*.py

@jvanhees

Copy link
Copy Markdown
Author

The current flow for this fix is far from elegant, I agree. Obviously we could add a flag to the CLI interface but I don't like that and it would require the Home Assistant integration to add another option to the setup wizard. There is an option to do a version call before logging in. I've added the command to my branch, but I don't have a LAN C so I am unable to see the difference, could you run this?

poetry run python -m aiocomfoconnect version --host <your host>

On the Pro, this is the output:

gatewayVersion  : 3222274053
serialNumber    : ENG<my serial>
comfoNetVersion : 1074790400

I've asked my digital junior developer for other options, this is the response:

Here are the remaining avenues worth exploring:

  1. VersionRequest serial prefix (most promising, needs LAN C data)
    Your Pro returned ENG<15 numbers>. LAN C serial numbers likely have a different prefix. This works pre-session, so the version command we just added would be the tool — just need someone with a LAN C to run it.

  2. gatewayVersion bit pattern
    Your Pro: 0xC0000005. The top byte 0xC0 (1100 0000) might encode the device variant. If a LAN C returns 0x80000005 or 0x40000005, the top bits would be a reliable flag.

  3. CnNodeNotification.productId (post-registration)
    Once connected, the bridge broadcasts node notifications that include a productId. Node 1 is the gateway itself — the Pro product number mentioned earlier was 30006323. This would be definitive but only works after a registered session is established.

  4. ListRegisteredAppsRequest pre-session
    Worth trying — some older firmware versions respond to this without a session. If it works and the response format differs between devices, that's a data point.

However, I'm not very hopeful we can differentiate with only 2 devices.

One other option is to call the HTTP endpoint that is exposed on port 80 on the device, and search the returned HTML for the text ComfoConnect PRO. But I'm not sure if that option is more elegant than the timeout solution.

I should have fixed the linting and formatting issues btw.

@jvanhees

Copy link
Copy Markdown
Author

I might try to get a pcap from the Zehnder application to see how it is handling it later this week, but we could also accept this fix for now and perhaps later (well.... you know ;) ) find a better way.

@michaelarnauts

Copy link
Copy Markdown
Owner

Using your version call times out on my Comfoconnect LAN C

It's because you are using the same source UUID and destination UUID. Oddly enough, this then works fine on the ComfoConnect PRO?

DEBUG:aiocomfoconnect.bridge:Connected to bridge 192.168.1.170
DEBUG:aiocomfoconnect.bridge:VersionRequest
DEBUG:aiocomfoconnect.bridge:Adding listener for event 1
DEBUG:aiocomfoconnect.bridge:TX 00000000000000000000000000000001 -> 00000000000000000000000000000001: 08122001
type: VersionRequestType
reference: 1

When I change your version code to be like this:

async def run_version(host: str):
    """Request version information from the bridge (no registration required)."""
    bridges = await discover_bridges(host)
    if not bridges:
        raise BridgeNotFoundException("No bridge found")

    bridge = bridges[0]
    await bridge.connect(DEFAULT_UUID)
    try:
        msg = await bridge.cmd_version_request()
        print(f"gatewayVersion  : {msg.gatewayVersion}")
        print(f"serialNumber    : {msg.serialNumber}")
        print(f"comfoNetVersion : {msg.comfoNetVersion}")
    finally:
        await bridge.disconnect()

It works, because the destination UUID is correct then.

DEBUG:aiocomfoconnect.bridge:Connected to bridge 192.168.1.170
DEBUG:aiocomfoconnect.bridge:VersionRequest
DEBUG:aiocomfoconnect.bridge:Adding listener for event 1
DEBUG:aiocomfoconnect.bridge:TX 00000000000000000000000000000001 -> 0000000000251010800170b3d54264b4: 08122001
type: VersionRequestType
reference: 1


DEBUG:aiocomfoconnect.bridge:RX 0000000000251010800170b3d54264b4 -> 00000000000000000000000000000001: 08442001 0881a8c0800c120d44454d30313136333731323034188080c0800c
type: VersionConfirmType
reference: 1

gatewayVersion: 3222279169
serialNumber: "DEM0116371204"
comfoNetVersion: 3222274048

DEBUG:aiocomfoconnect.bridge:Emitting for event 1
gatewayVersion  : 3222279169
serialNumber    : DEM0116371204
comfoNetVersion : 3222274048
DEBUG:aiocomfoconnect.bridge:Disconnecting from bridge 192.168.1.170

@michaelarnauts

Copy link
Copy Markdown
Owner

Also, calling the HTTP endpoint won't work, because the Comfoconect LAN C doesn't expose a HTTP server, and I don't like to try and wait for a timeout just to know what system you have.

But maybe the Comfoconnect PRO sends a specifc UUID (because in your code, it seemed to accept the default uuid)?

What do you see when you run

$ poetry run python -m aiocomfoconnect --debug discover --host <ip>

DEBUG:asyncio:Using selector: EpollSelector
INFO:asyncio:Datagram endpoint local_addr=('0.0.0.0', 0) remote_addr=None created: (<_SelectorDatagramTransport fd=6 read=idle write=<idle, bufsize=0>>, <aiocomfoconnect.discovery.BridgeDiscoveryProtocol object at 0x7b73387093a0>)
DEBUG:aiocomfoconnect.discovery:Socket has been created
DEBUG:aiocomfoconnect.discovery:Sending discovery request to 192.168.1.170:56747
DEBUG:aiocomfoconnect.discovery:Data received from ('192.168.1.170', 56747): b'\x12#\n\r192.168.1.170\x12\x10\x00\x00\x00\x00\x00%\x10\x10\x80\x01p\xb3\xd5Bd\xb4\x18\x01'
Discovered bridges:
<Bridge 192.168.1.170, UID=0000000000251010800170b3d54264b4>

DEBUG:asyncio:Close <_UnixSelectorEventLoop running=False closed=False debug=True>

@jvanhees

Copy link
Copy Markdown
Author

If I run that command:

DEBUG:asyncio:Using selector: KqueueSelector
INFO:asyncio:Datagram endpoint local_addr=('0.0.0.0', 0) remote_addr=None created: (<_SelectorDatagramTransport fd=6 read=idle write=<idle, bufsize=0>>, <aiocomfoconnect.discovery.BridgeDiscoveryProtocol object at 0x105f48e30>)
DEBUG:aiocomfoconnect.discovery:Socket has been created
DEBUG:aiocomfoconnect.discovery:Sending discovery request to 192.168.0.85:56747
DEBUG:aiocomfoconnect.discovery:Data received from ('192.168.0.85', 56747): b'\x12$\n\x0c192.168.0.85\x12\x10\x1f\xffz\x10z\x10@\x00\x80\x00\x14O\xd7\x10\x1a\xde\x18\x01 \x02'
Discovered bridges:
<Bridge 192.168.0.85, UID=1fff7a107a1040008000144fd7101ade>

DEBUG:asyncio:Close <_UnixSelectorEventLoop running=False closed=False debug=True>

@jvanhees

Copy link
Copy Markdown
Author

I think this might be the key to recognize the new version. It seems like it is identifying itself with a new GatewayType in the SearchGatewayResponse. I think I'm going to rewrite this PR so we handle the registration flow slightly different for either device.

@michaelarnauts

Copy link
Copy Markdown
Owner

Nice. What is the version of that response? On my device, it's 1. I think I don't have the gatewaytype, but I should check this.

@michaelarnauts

Copy link
Copy Markdown
Owner

The protob file I use specifies this

enum GatewayType {
    lanc = 0;
    season = 1;
}

Unsure what season is, but is the comfoconnect pro maybe 2? It might make sense to extract a new protob file from the android apk. That's where I got the current version. Your raw string seems to end with 2.

@jvanhees

jvanhees commented May 28, 2026

Copy link
Copy Markdown
Author

Yes, it's 2 indeed. But I think, looking at the current codebase, that this needs to be handled in main.py. The downside of that is that this also needs to be changed in the config flow of the homeassistant repository (https://github.com/michaelarnauts/home-assistant-comfoconnect/blob/master/custom_components/comfoconnect/config_flow.py). The downside is that this logic now resides in both repositories. I think it's up to you if you're ok with that, or if you want to somehow refactor this so the logic is inside the ComfoConnect class.

If we fix it in main.py, I'd probably add a "is_pro" branch after the connect call (https://github.com/michaelarnauts/aiocomfoconnect/blob/master/aiocomfoconnect/__main__.py#L92).

Edit: GatewayType would probably look like this:

enum GatewayType {
    lanc = 0;
    season = 1;
    pro = 2;
}

@jvanhees jvanhees force-pushed the comfoconnect-pro-connection branch from 80c36d2 to b64d45a Compare May 28, 2026 20:52

@jvanhees jvanhees left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've written a refactor that changes some of the function and introduces a new register method on the bridge. Could you test if this also works on the LAN C?

Comment thread aiocomfoconnect/bridge.py
Comment on lines +134 to +140
if self.bridge_type != GATEWAY_TYPE_PRO:
# LAN C: check whether we are already registered by starting a session.
try:
await self.cmd_start_session(True)
return False # Already registered; session is now active.
except ComfoConnectNotAllowed:
pass # Not registered yet; fall through to register below.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are ok with changing the actual behaviour, we could get rid of this part. I'm not really sure how that would work on the LAN C tho.

Comment thread aiocomfoconnect/bridge.py
_LOGGER = logging.getLogger(__name__)

TIMEOUT = 5
GATEWAY_TYPE_PRO = 2

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might want to get this from the protobuf file.

@jvanhees

Copy link
Copy Markdown
Author

Hi @michaelarnauts , have you had time to look at this and test it with the LAN C?

@Niek

Niek commented Jun 11, 2026

Copy link
Copy Markdown

I decompiled the current Zehnder Android app APK (link) and compared its local connection path with this PR. The high-level conclusion matches this PR: ComfoConnect PRO still speaks the same UDP discovery + TCP/protobuf protocol on port 56747, and the PRO discovery response uses gateway type value 2.

Concrete APK findings:

  • Discovery sends UDP payload 0a00 to the subnet broadcast address on port 56747.
  • The current APK's bundled zehnder.proto still only names GatewayType values lanc = 0 and season = 1. So pro = 2 is a live-traffic inference, not something the APK proto names. The inference is consistent with the PRO discovery response ending in protobuf field type = 2.
  • The app stores the actual UDP packet source address (DatagramPacket.getAddress()) as the discovered gateway address, rather than relying only on searchGatewayResponse.ipaddress from the protobuf body.
  • Discovery appears to be attempted twice with a short delay. That may explain the “first discovery often misses, second works” behavior mentioned above.
  • Local connection is plain TCP to the discovered address on port 56747; the frame format is unchanged: 4-byte length, 16-byte source UUID, 16-byte destination UUID, 2-byte operation length, GatewayOperation, optional body.
  • The APK also has active handlers for CnWhoAmI and Wi-Fi setup operations (WiFiSettings, WiFiNetworks, WiFiJoinNetwork). Those are not needed for normal ventilation control, but they are part of the official app's PRO/local setup path.

Suggested changes to this PR:

  1. In discovery, use the UDP sender IP (addr[0]) as the Bridge.host, or at least prefer it over parser.searchGatewayResponse.ipaddress. That matches the official app and is more robust if the gateway reports a stale/odd IP in the protobuf body.

  2. Add a small discovery retry, probably two sends with a short delay, matching what the app appears to do. This may fix the first-run discovery miss without involving registration logic.

  3. Since this PR adds pro = 2 to the generated protobuf enum, use the generated enum constant instead of a separate hard-coded GATEWAY_TYPE_PRO = 2 in bridge.py.

  4. Optional/future: add decode mappings/helpers for CnWhoAmI and Wi-Fi setup messages. Not required for the HA control path, but it would bring the library closer to what the current app supports for ComfoConnect PRO setup/commissioning.

So I think the registration-flow direction here is right, but the discovery host selection and retry behavior are still missing compared with the official Android app.

@FrancescoC87

FrancescoC87 commented Jun 16, 2026

Copy link
Copy Markdown

I decompiled the current Zehnder Android app APK (link) and compared its local connection path with this PR. The high-level conclusion matches this PR: ComfoConnect PRO still speaks the same UDP discovery + TCP/protobuf protocol on port 56747, and the PRO discovery response uses gateway type value 2.

Concrete APK findings:

  • Discovery sends UDP payload 0a00 to the subnet broadcast address on port 56747.
  • The current APK's bundled zehnder.proto still only names GatewayType values lanc = 0 and season = 1. So pro = 2 is a live-traffic inference, not something the APK proto names. The inference is consistent with the PRO discovery response ending in protobuf field type = 2.
  • The app stores the actual UDP packet source address (DatagramPacket.getAddress()) as the discovered gateway address, rather than relying only on searchGatewayResponse.ipaddress from the protobuf body.
  • Discovery appears to be attempted twice with a short delay. That may explain the “first discovery often misses, second works” behavior mentioned above.
  • Local connection is plain TCP to the discovered address on port 56747; the frame format is unchanged: 4-byte length, 16-byte source UUID, 16-byte destination UUID, 2-byte operation length, GatewayOperation, optional body.
  • The APK also has active handlers for CnWhoAmI and Wi-Fi setup operations (WiFiSettings, WiFiNetworks, WiFiJoinNetwork). Those are not needed for normal ventilation control, but they are part of the official app's PRO/local setup path.

Suggested changes to this PR:

  1. In discovery, use the UDP sender IP (addr[0]) as the Bridge.host, or at least prefer it over parser.searchGatewayResponse.ipaddress. That matches the official app and is more robust if the gateway reports a stale/odd IP in the protobuf body.
  2. Add a small discovery retry, probably two sends with a short delay, matching what the app appears to do. This may fix the first-run discovery miss without involving registration logic.
  3. Since this PR adds pro = 2 to the generated protobuf enum, use the generated enum constant instead of a separate hard-coded GATEWAY_TYPE_PRO = 2 in bridge.py.
  4. Optional/future: add decode mappings/helpers for CnWhoAmI and Wi-Fi setup messages. Not required for the HA control path, but it would bring the library closer to what the current app supports for ComfoConnect PRO setup/commissioning.

So I think the registration-flow direction here is right, but the discovery host selection and retry behavior are still missing compared with the official Android app.

Hi Niek,

Thanks for the deep analysis of the decompiled APK.

I'm wondering if your findings might shed some light on a persistent issue several of us are facing (detailed in home-assistant-comfoconnect#103). In my case, after a firmware update on my ComfoAir Flex, the integration started failing with an RMI error; before the firmware update the integration was working perfectly.

Using the aiocomfoconnect library directly, I am able to establish a connection to my unit without any issues, however the moment I try to send any command, the unit throws an RMI error.

Since you mentioned the presence of handlers like CnWhoAmI, did you happen to notice any changes in the new APK that could explain why commands are being rejected post-firmware update? For instance:

  • Are there stricter registration/handshake requirements (like requiring CnWhoAmI) before the unit accepts control operations?
  • Are there changes in how UUIDs are validated for the session?
  • Have the protobuf definitions for the control commands themselves changed?

Any insights you might have from the APK would be hugely appreciated!

Thanks.

@Niek

Niek commented Jun 16, 2026

Copy link
Copy Markdown

I compared this more directly with the APK.

I don’t see changed protobufs or changed bytes for the failing calls. The model read is still property 0x08 as a string, and the preset speed command still looks like schedule 0x15 / instance 1 / timer 1 / value 0..3.

The bigger mismatch seems to be node selection. The APK handles CnNodeNotification, maps productId 8 to COMFOAIRFLEX, creates the Flex node with the notification nodeId, and sends RMI using that stored node id.

This PR / aiocomfoconnect still treats CnNodeNotificationType as unhandled, and most RMI calls default to node_id=1. So if the Flex is not node 1 behind the PRO, the session can work but model/speed RMI calls will hit the wrong node and return RMI_ERROR.

So I’d look at using the ventilation unit nodeId from CnNodeNotification first, rather than CnWhoAmI, UUID/session changes, or protobuf changes. The APK also has productId 9 for COMFOAIRFLEXCONNECTIONBOARD; control commands should use the COMFOAIRFLEX node id, not necessarily node 1.

@FrancescoC87

Copy link
Copy Markdown

Thanks for the detailed analysis. You were absolutely right.

In my case (direct connection to a ComfoAir Flex, with the ComfoConnect gateway embedded in the unit itself), the problem turned out to be exactly the node_id.

I added the following logging to the CnNodeNotification handler:

elif message.cmd.type == zehnder_pb2.GatewayOperation.CnNodeNotificationType:

_LOGGER.info(
    "CnNodeNotification: nodeId=%s productId=%s zoneId=%s mode=%s",
    message.msg.nodeId,
    message.msg.productId,
    message.msg.zoneId,
    message.msg.mode,
)

This produced:

CN NODE NOTIFICATION - nodeId=11 productId=25 zoneId=1 mode=2
CN NODE NOTIFICATION - nodeId=41 productId=9 zoneId=1 mode=2
CN NODE NOTIFICATION - nodeId=45 productId=8 zoneId=1 mode=2

Based on your APK findings that productId 8 corresponds to the ComfoAir Flex ventilation unit, I modified the aiocomfoconnect library installed in Home Assistant and replaced all occurrences of node_id=1 with node_id=45 in comfoconnect.py and bridge.py.

After that, everything started working again immediately.

So it appears that after updating to the latest firmware, the ventilation unit is no longer exposed as node 1. The session establishment, registration, and property definitions are still correct, but all RMI requests were being sent to the wrong node, resulting in the RMI_ERROR.

The next step would be to automate what I did manually and implement it properly in the library. Ideally, aiocomfoconnect should discover and store the correct ventilation unit node id from CnNodeNotification before issuing any RMI request, including the initial get_property() calls performed during Home Assistant integration startup (for example in home-assistant-comfoconnect's init.py).

One thing I was wondering: from your APK analysis, is the mapping of productId 8 → ComfoAirFlex implemented through a predefined lookup table in the application, or does the app automatically determine the correct node to use based solely on the received CnNodeNotification packets?

In other words, does the app require prior knowledge of the selected unit type during the initial configuration of the application, or is the entire node selection process driven dynamically by the notifications received from the device?

I think it may be worth opening a dedicated issue for this, as it appears to be a separate problem from the ComfoConnect PRO gateway support addressed in this PR.

@Niek

Niek commented Jun 18, 2026

Copy link
Copy Markdown

I checked the product-id mapping a bit further.

The app appears to use a fixed product-id table to classify nodes, but the actual node id is learned dynamically from CnNodeNotification. So it does not look like the app needs the user/config flow to know upfront that the unit is a Flex. It receives the notifications, maps the product id, stores the node id, and sends later RMI requests to that node.

For this case, the important ids are:

  • 1 = ComfoAir Q
  • 8 = ComfoAir Flex
  • 9 = ComfoAir Flex connection board
  • 222 / 0xde = ComfoConnect PRO

Your productId=25 / 0x19 is not in the product-id table I found, so I would not use that for ventilation control.

Full product-id table I found
ID Hex Name
1 0x01 COMFOAIRQ
2 0x02 COMFOSENSE
3 0x03 COMFOSWITCH
4 0x04 OPTIONBOX
5 0x05 ZEHNDERGATEWAY
6 0x06 COMFOCOOL
7 0x07 KNXGATEWAY
8 0x08 COMFOAIRFLEX
9 0x09 COMFOAIRFLEXCONNECTIONBOARD
10 0x0a CO2SENSOR
13 0x0d COMFOVARNGMAINNODE
14 0x0e COMFOVARNGPERIPHERALNODE
20 0x14 COMFOCLIME
21 0x15 COMFODRY
22 0x16 COMFOPOST
222 0xde COMFOCONNECTPRO
253 0xfd SERVICETOOL
254 0xfe PRODUCTIONTESTTOOL
255 0xff DESIGNVERIFICATIONTESTTOOL

For aiocomfoconnect, I think the fix should be:

  • handle CnNodeNotificationType
  • store nodes by nodeId and productId
  • select the ventilation unit node from product ids 1 or 8
  • ignore product id 9 for control commands
  • wait for that ventilation node before the initial get_property(PROPERTY_MODEL)
  • pass the discovered ventilation nodeId into all RMI/property calls
  • keep node_id=1 only as a fallback for older setups where no notification is seen

That should fix this without hardcoding a specific node id like 45.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants