I bought a Halo Chlor 25 in early April. It is the salt chlorinator that sits at the centre of the AstralPool Halo Connect ecosystem, and most of an Australian pool’s smarts run through it: the heat pump talks to it, the lights talk to it, the pH and ORP probes talk to it. If you want a Home Assistant integration for an Astralpool setup, the Chlor 25 is the device you target.
For a few days the integration worked. I could read pool temperature and chemistry. I could turn the heat pump on when the solar inverter said there was surplus going to the grid. The community-maintained pychlorinator-cloud integration (Rob Markoski’s work, with prior reverse engineering by Daniel Nagy) handled the BLE handshake to the chlorinator and exposed everything as Home Assistant entities. It was the kind of small, satisfying piece of household plumbing that makes the whole local-first effort feel worth it.
Then a firmware update went out. The integration stopped working. Every BLE pairing attempt failed at the authentication step.
The change wasn’t a bug fix. It is, on inspection, a deliberate gate. The practical effect of the gate is to keep third-party software away from hardware the customer owns.
This is the story of that gate, the wall it puts in front of every Halo owner who wants to use something other than the Astralpool app, and the punchline. The same app that demands Google Play Integrity attestation to keep me out of my own chlorinator prints the cloud password in plain text to the system log of any Android phone running it.
A short statement of what this article is and isn’t, before I get into anything technical:
Disclosure. This article describes interoperability research conducted under the Copyright Act 1968 s 47D, which permits decompilation of a computer program to obtain information necessary to make an interoperable program. No Play Integrity bypass is shared in this article (none exists, by the design of attestation). Credentials, serial numbers, and log excerpts are redacted throughout. The fork referenced at the end is intended for use by owners of the hardware on hardware they own.
Why this is a story before it’s a hack
There is a thing that keeps happening to people who own consumer hardware in 2026. You buy a device. It works. The vendor pushes a firmware update over the air. The device still works, but a thing you used to be able to do, often a thing you used to be able to do with software you wrote yourself or paid someone else to write, no longer works. The hardware is the same. The vendor has changed the software in a way that benefits the vendor and removes something from you.
I am the customer. The chlorinator is mine. The pH and ORP readings are about the water in my pool. The heat pump it controls draws electricity I pay for. The decision about whether to send those readings to a cloud service, hand them to my own software, or do both is a decision that should belong to me.
What Astralpool’s firmware 2.7 did, and what I am about to walk through, was to take that decision away. The chlorinator’s local Bluetooth interface still exists. The crypto on the device side has not changed. The integration that used to talk to it knows the same handshake it always did. What changed is that a server-side check now sits between the phone (or any client) and the device, and that check refuses to authorise any client that is not the genuine Astralpool app, installed from the Play Store, on a non-rooted phone, sending a fresh Google-attested integrity token.
It is not a security improvement for the owner. It does not stop a malicious actor from doing anything they could already do. The only thing it stops is owners using their own hardware with software they choose. That is the political shape of the change. The technical shape is below.
The wall
When the pychlorinator-cloud integration started failing, I did the obvious thing first: checked the BLE advertisements directly. I wrote a small script (halo_ble_dump.py) using the bleak library to dump the manufacturer data the chlorinator emits. The advertisement layout was unchanged. Manufacturer ID 1095, the access code field at the same byte offset as before. So whatever changed wasn’t the advertisement format.
The next step was the handshake. The integration’s existing flow assumed the device used a local AES path with a key derived from the access code. I dumped raw Bluetooth traffic during a successful Halo Chlor Go app session and confirmed the device-side cryptography was identical. The chlorinator was running the same handshake it always had.
The change had to be on the client side. So I pulled the official Astralpool app’s APK off a phone, extracted the .NET assemblies (the app is a Xamarin / .NET Mono build, which is relevant later), and decompiled them. I will not be sharing the decompiled source. What I will share are the file paths, class and method names, and a description of the control flow, which is what someone trying to interoperate with this hardware actually needs.
Two files matter. The first is AstralPoolService.BusinessObjects/Device.cs. Around lines 895 through 905 there is an authentication branch that reads DeviceProtocolRevision off the connected device and routes the auth flow accordingly. Older firmware advertises a zero or absent revision, and the local AES path runs. Firmware 2.7 advertises a non-zero revision, and the auth flow takes a different branch.
The second file is BusinessObjects.Helpers/MacHelper.cs. The new branch routes through a method called RequestMac, which makes a pair of HTTPS POSTs to:
https://halo.connectmypool.com.au/halo/bluetooth-auth-key/version-1/generate-challenge
https://halo.connectmypool.com.au/halo/bluetooth-auth-key/version-1/request-mac
The body of those requests includes a Google Play Integrity token (or, on iOS, an Apple App Attest token). The server validates the token with Google’s (or Apple’s) attestation service and, if it is satisfied that the request comes from the genuine, store-installed Astralpool app on a non-rooted device, returns a per-session MAC that the client then uses to complete the BLE handshake with the chlorinator.
That last sentence is the gate. The chlorinator will not accept the BLE handshake without that MAC. The MAC is only issued to clients Google’s attestation service has stamped as the genuine Astralpool app. There is no way for a third-party BLE client to get one. That is the whole point of attestation: it is a signature that an integrity gatekeeper, not a developer, controls.
Two things follow.
One: the chlorinator’s local cryptography is unchanged. The integration that worked yesterday still has the right code. Astralpool didn’t change the lock. They added a doorman who only opens the door for staff in uniform.
Two: there is no way around the attestation. Some readers are already typing the words “frida” or “magisk hide” or “Play Integrity Fix module” into the comment field. None of those work for any length of time. Google’s Play Integrity attestation is specifically designed to detect rooted devices, modified app binaries, and runtime instrumentation. Bypasses exist for short windows; Google patches them; the cycle continues. I spent a few hours reading those threads and concluded what every honest writer on the topic concludes: if your security model depends on bypassing Play Integrity from a rooted phone in production, your security model is unwise. I am not going to publish a fragile bypass that is going to break next month and get someone’s HA install knocked over. I went looking for a different path.
The realisation
Here is the thing I missed for an embarrassingly long stretch. The Halo Chlor Go app does not need to talk to the chlorinator over BLE every time you tap a button. Most of what the app does is talk to Astralpool’s cloud, and the cloud talks to the chlorinator. The BLE pairing is a one-time bootstrap that establishes cloud credentials. After bootstrap, the chlorinator phones home over Wi-Fi, the cloud knows its serial number, and the app reads from and writes to the cloud.
If I could get the cloud credentials, I would not need BLE at all. The Play Integrity gate would still be there. I just wouldn’t have to walk through it.
So: where does the app store cloud credentials?
The app is a Xamarin build. Xamarin apps run a Mono runtime on Android. Mono’s Console.WriteLine, the same Console.WriteLine you would write in any .NET console application, does not vanish on Android. It writes to a tag called mono-stdout, which is part of the standard Android log stream. Anyone with a USB cable, Android debugging enabled, and adb can read that stream from any phone running the app.
I plugged in. I ran adb logcat -s mono-stdout. I force-stopped the Halo Chlor Go app and reopened it. I grepped for the word “Credentials”.
The app, on every cold start, prints a JSON payload to standard output. The payload looks like this, with placeholder values:
Credentials Message: {"users":[{"sn":"xxxxxxx","username":"xxx-xxx","password":"xxxx...xxxx"}]}
The sn is the chlorinator’s serial number. The username and password are the cloud credentials Astralpool’s servers will accept for that chlorinator. The password is sixty-four characters long. They are bearer credentials in the strongest sense: anyone holding them can log into the cloud and control the device.
The same app that demands Google Play Integrity attestation to authorise a single BLE handshake prints those bearer credentials in plain text to the system log on every launch.
I read that line and I sat there for about a minute.
It is hard to overstate how thoroughly the attestation gate is undone by this. The whole point of Play Integrity in this design is that the cloud should only trust requests from a genuine, untampered, attested app. Once the credentials are in your hand, the cloud doesn’t care which app you are. Curl works. Python works. Home Assistant works. The attestation gate is wallpaper over a hole in the floor.
I am not the first person to point out that mobile apps shipping bearer secrets to the device is a pattern that defeats most of the protection it is supposed to provide. The OWASP Mobile Top 10 has been making this point for a decade. What is striking here is the contrast: an enormous amount of engineering effort went into the Play Integrity gate. Far less, by the look of it, went into checking what the app prints to standard output.
The fork
With the credentials in hand, the rest of the work was straightforward.
The pychlorinator-cloud integration already speaks Astralpool’s cloud protocol. It just expected to obtain the credentials via the BLE pairing flow that no longer works. I forked the repository (davidbell81/pychlorinator-cloud, branch fw27-cloud-only-control) and added a manual credential entry path. The user supplies the serial, username, and password from the logcat extraction. The integration skips BLE entirely and goes straight to the cloud.
It works. All the entities the original integration exposed come back: cell mode, heater control, setpoint, light mode, light colour, water temperature from the connected probe, error and info channels. Solar-match pool heating works again. The article on the pool control loop will go up next week.
The pull request is open upstream at robmarkoski/pychlorinator-cloud#2. If you have a Halo with firmware 2.7 or later and you want this working today, the fork is where to start. The README walks through the logcat extraction and manual entry steps.
There is one operational wrinkle. Astralpool’s cloud allows exactly one concurrent connection per chlorinator, regardless of which user account makes the connection. If your phone is logged in to the Halo Chlor Go app and you bring up the Home Assistant integration, one of them will be kicked. The constraint appears to be on the cloud side; the chlorinator itself doesn’t enforce it. Workaround: disable the HA integration entry (don’t delete it) when you need the phone, re-enable when you’re done. It is annoying, but the data flow once you accept the constraint is rock solid.
What this means, and what I am asking the next reader to take from it
I am not the first person to write that vendors are using software gates to enforce lock-in on consumer hardware. The FTC sued John Deere in January 2025 over exactly this pattern in tractors, where only authorised dealerships could access the diagnostic software needed to complete a repair. Apple has been pairing component serial numbers to phone serial numbers for years; the company announced a partial reversal in April 2024 only after sustained legislative pressure. The Halo Chlor 25 is now, on the evidence of firmware 2.7, in that company.
What I want this article to be useful for is the next person. If you are a Halo owner trying to use Home Assistant with your pool, you have a path. If you are a developer maintaining a community integration that just stopped working after a vendor firmware update, you have an example of what the gate looks like and where to start. If you are writing about right-to-repair and you wanted a clean Australian example of a vendor adding a software gate that benefits no one but them, you have one.
There is also a lesson for vendors thinking about doing this. Play Integrity is not a magic immunity charm. It is a server-side check that depends on every other thing your app does being consistent with the security posture you are trying to project. If the same app that demands attestation also writes the bearer credentials to a log stream that any owner with a USB cable can read, you have built a fortress with a glass back wall.
I am not going to argue that what Astralpool did was malicious. I do not have evidence of intent. I have evidence of effect. The effect of firmware 2.7 is to take a decision that belonged to the owner of a piece of pool equipment and move it inside Astralpool’s app. The effect of Console.WriteLine is that the decision can be moved back. Both of these are facts about software the company shipped.
If anyone at Astralpool is reading: the fix for the leak is one line. The fix for the lock is to remove it.
Repo: davidbell81/home_automation (Home Assistant config, BLE dump scripts).
Fork: davidbell81/pychlorinator-cloud, branch fw27-cloud-only-control. Upstream PR: robmarkoski/pychlorinator-cloud#2.
Sibling article: Heating a pool around a car and a roof, the solar-match control loop this integration restores.