Viron Astral Pool ChlorinatorGo integration

@Mikkaat, I had a look at your log files. Seems to be predominantly failing when trying to connect.
Can you try updating your astralpool_chlorinator version to 0.1.7 and see if there is any change in the behaviour?

Have just updated the firmware on my Chlorinator and the app from the HACS repo (it just showed up now)…

I will allow it to run a while and let you know.

how does one upgrade the firmware

@sam43434
Open up HACS
image

Choose Integrations

Select Astral Pool Viron eQuilibrium Chlorinator (if you have already installed it. If you haven’t installed it yet, then a different process applies).

In the top-right menu, choose Redownload

Select the latest version in the dropdown, and click Download.

You will need to restart home assistant.

Wow, can I just say I’ve been on home assistant for 5 years and never felt the need to post… until now!

This is a fantastic hack, I’ve been looking to integrate the Chlorinator for a few years now, with the catch being that my EQ25 didn’t have bluetooth until last month when it died and I replaced the box. I had considered the Astral connect gateway, but it seemed overly expensive for an interface.

As I’ve hacked my fair share of things into home assistant, this is not a small thing and the fact that you took the time to provide it into HACS to share it with us is great (looks for the buy him a coffee button).

Given I’ve only just configured mine and it mostly working, I don’t have any feedback that can really assist just yet. If I find anything I’ll be sure to post it!!!

Anyways massive thanks @pbutterworth

2 Likes

I wonder if the newer Astral Halo system has the same Bluetooth design and operation? Guess I should try this out and see. BLE looks very similar but last digit is 2 and not 1?

Hi @farsonic, I’m fairly confident that pychlorinator won’t talk to that service UUID.
But I had a look back in the decompiled android C# code and there is lots of this sort of thing:

					if (e.get_Characteristic().get_Uuid() == "45000002-98b7-4e29-a03f-160174643001")
					{
						val = GetCharacteristic("45000003-98b7-4e29-a03f-160174643001");
					}
					else if (e.get_Characteristic().get_Uuid() == "45000001-98b7-4e29-a03f-160174643002")
					{
						val = GetCharacteristic2("45000002-98b7-4e29-a03f-160174643002");
					}

It’s deciding between two different protocols based on the service UUIDs. I haven’t dug any deeper.
If anyone is interested in extending pychlorinator to talk, I’m happy to send through the decompiled C# code from the ChlorinatorGo app

ok, the App I use is totally different though…

Looks like the app probably shares quite a bit of code with other app.

I wouldn’t think astral would reinvent the wheel, but interesting they released a new app of the halo system.

On a different topic, these unavailable sensors are annoying me, I’m trialling a code fix (more bandaid) to retain the last value if the async_gatherdata return an empty dict. I’ll let you know how I go over night, but I think I’d want to put a limit of 5 reads and force it back to empty. its more for neatness of the sensors than anything, the root cause still looks to be the bluetooth stability.

Thoughts on how long the data is stale before it should be forced to unavailable? (5 reads is ~5mins)

Edit: Well it looks like every failure is related to the connection failing, I’ll have a look tomorrow maybe how the error is handled

Yep - this is what i’ve found (at least with the HaloChlor model).

First off, nice work @pbutterworth at getting this going. It had never occurred to me to decompile the app and I was going to do the time to attempt to reverse engineer the protocol from bluetooth sniffing. Given the encryption, this would have failed - so very grateful you cracked the nut first.

I’ve just got my development environment going, so hoping I can sort this out soon. I’ve found (from the decompilation) that the HaloChlor uses a different secret key, but same encryption method. IT also uses a different handshake protocol to initiate the session (as you mention above), but overall it looks like the rest of the comms are similar. I’ll try and get something working and post back here once I’ve got more info.

But @DunkeyDaMonkey, it would be worth decompiling your app and seeing how it communicates with the unit.

1 Like

I’m on the ChlorinatorGO app as everyone else on this thread as I have the astral EQ25. From my own integration I see a lot of data loss from the BT comms, so I’m just looking into a bit of global error handling on the python side to reduce the or remove the data loss, 1 minute polling is great, but if the data is “unavailable” 10% of the time, there will be times when I’m viewing my dashboard an not be able to see the values. in those cases I’d rather see the value from the last 5mins if its available.

@DunkyDaMonkey,
Firstly, sorry for the rubbish code - I’m new to python. And, please excuse the hasty code. There’s no exception handling, and many places where it is quite brittle.

Yeah, the 1 minute polling was pretty arbitrary…
Ideally, the python api would handle trying harder to connect before failing. As I understand it, bleak_retry_connector does a lot of this handling somehow (as described here). It’s beyond me though; the documentation is a bit sparse.

When HA first loads the chlorinator entry on startup, in async_setup_entry, it gets a BLEDevice from the saved MAC address. This is passed to the instance of the chlorinator API and reused ‘forever’. If this fails, HA automatically retries (I think).

The problem occurs later in the polling event of the coordinator: _async_update_data, which calls async_gatherdata, which creates a BleakClient from the saved BLEDevice. This is where it actually tries to connect to the device, and is liable to fail to connect without any retry (I think). This needs something more resilient. I raked over lots of different repos using bleak, but struggled to find an obvious way to do it.

This characterises the most common failure mode:

2023-04-05 06:03:20.893 ERROR (MainThread) [custom_components.astralpool_chlorinator.coordinator] Unexpected error fetching astralpool_chlorinator data: [org.bluez.Error.Failed] le-connection-abort-by-local
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/helpers/update_coordinator.py", line 239, in _async_refresh
    self.data = await self._async_update_data()
  File "/config/custom_components/astralpool_chlorinator/coordinator.py", line 39, in _async_update_data
    data = await self.chlorinator.async_gatherdata()
  File "/usr/local/lib/python3.10/site-packages/pychlorinator/chlorinator.py", line 136, in async_gatherdata
    async with BleakClient(self._ble_device, timeout=3) as client:
  File "/usr/local/lib/python3.10/site-packages/bleak/__init__.py", line 433, in __aenter__
    await self.connect()
  File "/usr/src/homeassistant/homeassistant/components/bluetooth/wrappers.py", line 249, in connect
    connected = await super().connect(**kwargs)
  File "/usr/local/lib/python3.10/site-packages/bleak/__init__.py", line 471, in connect
    return await self._backend.connect(**kwargs)
  File "/usr/local/lib/python3.10/site-packages/bleak/backends/bluezdbus/client.py", line 190, in connect
    assert_reply(reply)
  File "/usr/local/lib/python3.10/site-packages/bleak/backends/bluezdbus/utils.py", line 20, in assert_reply
    raise BleakDBusError(reply.error_name, reply.body)
bleak.exc.BleakDBusError: [org.bluez.Error.Failed] le-connection-abort-by-local

No need to be sorry!!! I think what you have created is fantastic. Even as some who’s job is 50% coding in python, I’m a complete novice and I’m not about the judge anyone code (I mean I still actively avoid class structures where possible as I originally learn to code in top down compliers)

Without going down the rabbit hole too far, as by the time you get to bleak you are about 3 layers deep in the libraries. The “fix” I deployed to my own integration is to just put a simple try except statement in around the API, in the even that we get nothing or we error we retain the last values and carry on. (if you wanted to we could initiate a wait and retry in the function but may require a bit of a restructure change)

This isn’t the best fix for what we are seeing, but it does make it more resilient. To be honest I’m a little too lazy to fork your repo to make the changes, if you want to give me perms I can branch my changes or I can just post my code blocks in an issue on git for you to have a look at.

And finally if it reassures you, I’ve written and reviewed much worse code than what you’ve put together.

v0.1.9 now has @DunkyDaMonkey’s proposed changes. Logbook looks much cleaner now!

3 Likes

Thank you to all who have contributed. I will update and test now.

I like the added log errors you added :slight_smile:

I also used it when I cleaned my pool earlier today, I found the selector can get our of sync with the manual buttons on the unit. I’ll raise an issue on git, but I haven’t had a look to digest the selector function. (the issue was I had turned it off manually to do a backwash and while I was turning my selector valve the pump kicked back on to auto from the app).

No harm done, it didn’t do it after i pulled out my phone and pushed the selector to off while I used the maint backwash function.

Thanks for all you efforts.

Yikes, that’s not ideal.
I’ll do some testing on mine to see if I can reproduce.
Will follow up on github.

Oh in case anyone else was interested, I record my acid feed tank level using a load cell, and temp and level of the pool using a water proof thermocouple and dual waterproof ultrasonic sensor. I plan on moving those things back onto the ESP32 from the ESP8266, then basically all my Pool stuff will come back via the one chip housed in a IP65 rated box next to the chlorinator.

Food for thought. I’m game for more ideas, but I can’t think of any more.

The load cell for the acid is awesome!

How about voltage and current sensing on the cell?
Filter pressure?
Pump RPM?