Eurotronic Comet WiFi: Ditching the Cloud for Local Home Assistant Control

I have three Eurotronic Comet WiFi thermostats. They work fine. The app works fine. But every morning I’d watch my Home Assistant logs fill up with connection attempts going out to some mqtt3.eurotronic.io endpoint and think: this is not how I want my heating to work. My radiators should not depend on a server somewhere in Germany staying online. So I fixed it.

This post is the writeup I wish existed when I started. It took a weekend of packet sniffing, failed HACS integrations, and one very satisfying Pi-hole rule to get here.

What’s Actually Inside These Things

The Comet WiFi runs on a Dialog Semiconductor DA16200 WiFi chip. That’s important because it immediately rules out a few popular options: no Tuya compatibility, no custom firmware path, nothing in ESPHome. The DA16200 is not an ESP8266. You’re not flashing this thing.

What the thermostat does do is speak MQTT — and only MQTT — to a fixed set of cloud hostnames: mqtt.eurotronic.io, mqtt1.eurotronic.io through mqtt5.eurotronic.io. No local API. No mDNS. No REST endpoint. Just MQTT, pointed permanently at the cloud.

The moment I confirmed this with Wireshark I knew exactly what to do.

The Interception: Pi-hole + Local Mosquitto

The plan is elegant: intercept the DNS queries for those cloud hostnames and point them at your own Mosquitto broker. The thermostats never know the difference. They connect, authenticate, and start chattering away — just to your broker instead of Eurotronic’s.

In Pi-hole, add custom DNS records for every variant:

mqtt.eurotronic.io   → 192.168.0.10
mqtt1.eurotronic.io  → 192.168.0.10
mqtt2.eurotronic.io  → 192.168.0.10
mqtt3.eurotronic.io  → 192.168.0.10
mqtt4.eurotronic.io  → 192.168.0.10
mqtt5.eurotronic.io  → 192.168.0.10

Replace 192.168.0.10 with whatever IP your Mosquitto instance is running on. Don’t skip any of the numbered variants — the firmware will try several of them before giving up, and you want all paths leading home.

On the Mosquitto side, since all devices in a single Eurotronic installation share the same MQTT username and password (yes, really — one credential set for all your thermostats), and since you now own the broker, you can simply enable anonymous access:

allow_anonymous true
listener 1883

Counterintuitively, this is actually more secure than the cloud setup. Before, your thermostat data was transiting someone else’s infrastructure. Now it never leaves your LAN.

The MQTT Protocol

Once the thermostats are talking to your broker, subscribe to # and watch the traffic. You’ll see topics structured like this:

02/PREFIX/MAC/V/A0   ← values coming FROM the device
02/PREFIX/MAC/S/A0   ← commands going TO the device

The PREFIX and MAC are device-specific — just watch the broker traffic after your thermostats connect and you’ll spot them immediately. The registers you actually care about:

  • A0 — target temperature (setpoint)
  • A1 — current measured temperature
  • A5 — window open detection
  • A6 — battery level

Temperature values are encoded as hex, prefixed with #, where the hex value equals temperature × 2. So 21.0°C becomes #2a (42 in decimal, 0x2a in hex). 18.5°C is #25. Weird encoding, but consistent.

For polling, publish to the S/AF topic. Two useful payloads:

  • #01000000 — returns the current active setpoint cleanly
  • #02000000 — triggers an immediate current temperature report

You might find documentation elsewhere suggesting #0b on S/A0 for polling. I did too. It’s slow, unreliable, and sometimes returns schedule data mixed in with the current value. Avoid it. The AF approach is much cleaner.

One critical rule: never use the retain flag on any messages you publish. Retained messages get replayed to the thermostat every time it reconnects — which means your retained setpoint command will constantly override whatever the device’s internal heating schedule is trying to do. It’s a subtle bug that’ll have you wondering why your thermostat is ignoring its schedule.

Home Assistant Integration — Skip the HACS Plugin

There’s a HACS integration called comet_wifi_integration. I tried it. It has bugs, and more critically it uses QoS 2 for MQTT delivery, which HA’s MQTT client handles poorly. Messages get dropped, entities get stuck, it’s frustrating.

The better approach: use Home Assistant’s built-in MQTT climate entity via YAML. It’s rock solid and gives you full control.

In your mqtt.yaml:

climate:
  - name: Wohnzimmer
    temperature_command_topic: "02/PREFIX/MAC/S/A0"
    temperature_command_template: >-
      {{ "#%02x" % ((value | float * 2) | int) }}
    temperature_state_topic: "02/PREFIX/MAC/V/A0"
    temperature_state_template: "{{ int(value[1:3],base=16)/2 }}"
    current_temperature_topic: "02/PREFIX/MAC/V/A1"
    current_temperature_template: "{{ int(value[1:3],base=16)/2 }}"

The templates handle the hex encoding automatically. The command template converts a float like 21.0 into #2a. The state templates do the reverse. Swap PREFIX and MAC with your actual device values, repeat for each thermostat.

For polling, add an automation that fires every 15 minutes:

alias: Poll Comet WiFi Thermostats
trigger:
  - platform: time_pattern
    minutes: "/15"
action:
  - service: mqtt.publish
    data:
      topic: "02/PREFIX/MAC/S/AF"
      payload: "#01000000"
  - service: mqtt.publish
    data:
      topic: "02/PREFIX/MAC/S/AF"
      payload: "#02000000"

Battery and window sensors follow the same pattern using MQTT sensor and binary_sensor entities — same topic structure, same hex decoding.

One Thing to Keep in Mind

The Comet WiFi has an internal heating schedule that runs independently of anything you do via MQTT. If you set a temperature through Home Assistant, it’ll hold — until the thermostat’s next scheduled time slot kicks in and overrides it. This is the same behavior you’d get with the official app. It’s not a bug in your setup; it’s just how the device works. Plan your automations around it, or accept that the schedule has the final word.

The Result

Three thermostats, fully local. Real-time temperature readings in Home Assistant. Target temperature control. Battery monitoring. Window-open detection. Zero cloud dependency. The whole setup survives internet outages without a hiccup.

How We Got Here

Full disclosure: I built this integration in a pair-programming session with Claude Code, Anthropic’s CLI coding assistant. Claude handled the broker setup, DNS configuration, and initial HA integration code — but the existing community documentation and the HACS plugin both had gaps that only showed up during real-world testing. The #0b polling command that everyone recommends? Unreliable. The QoS 2 MQTT subscriptions in the custom integration? Silently broken in HA. The retain flag? A landmine waiting to blow up your heating schedule.

Each of these issues required hands-on debugging — me watching thermostat displays, checking if temperatures actually changed, opening windows to test sensors — while Claude analyzed the MQTT broker logs and iterated on the configuration. The S/AF polling commands and the distinction between #01000000 and #02000000 came from sniffing what the official Eurotronic app actually sends, which turned out to be completely different from what the community had documented.

It took more reverse engineering than it should have — Eurotronic publishes nothing about this protocol — but once you have the DNS intercept in place and understand the hex encoding, the rest falls into place quickly. If you’re sitting on a pile of Comet WiFi thermostats wondering why there’s no clean local integration, this is your path forward.

P.S. — The Jinja2 Trap

If your battery sensors show suspiciously low values, check your template. In Python, int("3C", 16) means „parse as base 16“ and returns 60. In Jinja2, the same syntax means „use 16 as the default if parsing fails.“ The correct Jinja2 for hex conversion is {{ value[1:] | int(base=16) }}, not {{ int(value[1:], 16) }}. This applies to battery values but not temperatures — the temperature registers happen to use only digits 0-9 in their hex encoding at typical room temperatures, so the bug is invisible until a value contains A-F.

Markus & Claude

Successfully used @AnthropicAI Claude Code to develop mainline Linux kernel patches for the CoolPi CM5 GenBook (RK3588)! 🎉Patches available at: https://github.com/marfrit/misc_patches

Now tackling the Radxa Rock 5 ITX+ — dual 4K display support on mainline is next. Huge thanks to @Collabora for their incredible upstream RK3588 work 🙏

And suspend for the GenBook is next as soon as an appropriate UART cable arrives…

coolpi loader

Das Mysterium des GenBook Boots ist gelöst: das OEM image ist eines für unterschiedliche Geräte des Herstellers. Der Bootloader des Herstellers ist Gerätespezifisch und ändert extlinux.conf so, dass der richtige Device Tree geladen wird. Gewöhnungsbedürftiger Hack!

cool-pi GenBook

[    0.000000] Booting Linux on physical CPU 0x0000000000 [0x412fd050]
[    0.000000] Linux version 6.18.6-1-aarch64-ARCH (builduser@arch-nspawn-106937) (aarch64-unknown-linux-gnu-gcc (GCC) 15.2.1
20251112, GNU ld (GNU Binutils) 2.45.1) #1 SMP PREEMPT_DYNAMIC Mon Jan 19 13:22:47 UTC 2026
[    0.000000] random: crng init done
[    0.000000] Machine model: CoolPi CM5 GenBook

Something strange – the original boot loader seems to overwrite extlinux.conf first 27 bytes with

default coolpi_rk3588_gbook

but extlinux treats the following line

default arch

as the one being evaluated.

LVM RAID6

Do. Not. Use. At this time, a LVM raid array cannot be reconstructed with a missing physical volume. That’s a 0% chance of data recovery. Which is less than BTRFS‘ 50% chance of RAID6 recovery.

Hasu USB to USB Controller Converter » 1upkeyboards

  Turn almost any USB keyboard into a programmable keyboard! This converter, created by Hasu, allows you to change the keymap and add functions through TMK firmware. NO soldering required. Externally attached. Add up to 7 layers and up to 32 Fn keys. Supports 6KRO (or NKRO keyboards that will work in 6KRO mode). Media/System control keys and ‘Fn’ key are not recognized by the converter, but will still function as originally programmed on the board.   Please check Hasu’s geekhack thread below for the current list of compatible and incompatible keyboards as well as additional information.

Quelle: Hasu USB to USB Controller Converter » 1upkeyboards

German characters, Linux and Windows

Just in case you haven’t noticed: I’m german, and that requires to type some german characters, like ä, ö and ü (and ß) from time to time.

One problem with that: qmk takes a keypress and translates it to a keycode to be sent to the operating system. The operating system takes the keycode and translates it to a character based on the selected keyboard layout. So, if you press „Z“ on an US keyboard, it emits keycode 52, which will be translated to the letter „Z“ using the US keycode-to-character translation map.

On a german keyboard, it gets translated to „Y“. But also, SHIFT+2 is translated to @ with the US layout, and to ‚“‚ (double quote) with the german layout.

To make things worse, these translations are not consistent across operating systems; the key combinations with „AltGr“ (the right Alt key, which is different from the left Alt key in the german layout) are not the same translations on Linux and Windows.

My solution is to use the US International layout with Windows and Linux, and to remap the keys in Linux using xmodmap:

keycode 24 = q Q q Q adiaeresis Adiaeresis at Greek_OMEGA q Q
keycode 26 = e E e E EuroSign EuroSign e E e E
keycode 29 = y Y y Y udiaeresis Udiaeresis leftarrow yen y Y
keycode 33 = p P p P odiaeresis Odiaeresis thorn THORN p P
keycode 39 = s S s S ssharp U1E9E U017F U1E9E s S

This is the bare minimum needed for the umlauts to work; still need to find the switch for Windows not to treat the double quote as dead character though.

NVRM: RmInitAdapter failed!

The last couple of days I was having trouble with my GTX 980M NVIDIA (Optimus) integrated graphics card (with neon / ubuntu). The driver nvidia would load, but refuse to work. NVIDIA settings would let me switch the graphics card, but nothing happened. dmesg / journalctl contained the following lines:

kernel: NVRM: GPU 0000:01:00.0: Failed to copy vbios to system memory.
kernel: NVRM: GPU 0000:01:00.0: RmInitAdapter failed! (0x30:0xffff:663)
kernel: NVRM: GPU 0000:01:00.0: rm_init_adapter failed, device minor number 0

After installing various (known to work previously) distributions, killing my bootloader in the process (yeah, thanks UEFI) I got desperate and installed Windows 10. The card showed up with a yellow exclamation mark in the device manager, stating the card couldn’t be used because it failed starting up with „Error 43“.

Some googling suggested to try the card in another motherboard, but I was afraid that would have been a bit much for an integrated graphics card. After some more googling, I tried to use „nvflash64“ – first with the wrong bios (fortunately, nvflash refused to flash the wrong image and yielded the ID I needed to search for the right one). I found the vbios file at techpowerup. Flashed the right file and… The exclamation mark was gone! Subsequently, when I restored my linux partition (an adventure of its own, due to my stupidity), NVIDIA Settings is working again! Yay!