Skip to main content

aioaquarite

Buy Me a Coffee PyPI version Python versions Tests License: MIT Maintainer Open issues GitHub stars

Async Python client for the Hayward Aquarite pool API.

This library provides a standalone API client for interacting with Hayward Aquarite pool equipment via the Hayward cloud service. It is designed to be used as the backend for the Home Assistant Aquarite integration.

Features

  • Auth: email/password sign-in against Firebase Identity Toolkit, with automatic token refresh.
  • Read: list pools, fetch full pool documents, read individual fields with type coercion.
  • Write: atomic single- or multi-field commands (set_value / set_values), with the local cache kept in sync so back-to-back writes never revert each other.
  • Real-time: resilient Firestore subscriptions (pool data and the user's pool list) with automatic token-refresh reconnects, exponential backoff, and connection-health reporting (on_health callback / healthy property).
  • History: pull stored sample series (pH, ORP, temperature, filtration, aux relays, …) and check clock drift against the Hayward backend.
  • Typed errors: every failure mode raises an AquariteError subclass, so callers only need one except clause.

Async model

Document reads (get_pools, fetch_pool_data) run on the Firestore AsyncClient and are awaited directly. The command and history endpoints (set_value, set_values, send_command, get_pool_stats, get_server_date) are plain aiohttp calls. Neither path blocks the event loop or dispatches to a thread pool.

The real-time listener is the exception: google-cloud-firestore implements on_snapshot only on its synchronous client — AsyncDocumentReference.on_snapshot raises NotImplementedError (checked against 2.29.0). subscribe_pool, subscribe_user_pools and the subscription teardown therefore run the synchronous client through asyncio.to_thread, and your snapshot callback is invoked from the Firestore background thread (hand data back with loop.call_soon_threadsafe, as the examples below do). This is an upstream limitation, not a design choice; it will be revisited if upstream implements an async listener.

Both clients are built from the same credentials, rotated together on token refresh, and released by AquariteAuth.close().

Installation

pip install aioaquarite

Requires Python 3.12+.

Quick start

import aiohttp
from aioaquarite import AquariteAuth, AquariteClient

async with aiohttp.ClientSession() as session:
    auth = AquariteAuth(session, "user@example.com", "password")
    await auth.authenticate()

    # Stable Firebase UID (`sub` claim of the id token); useful as a
    # config-entry unique_id. Returns None before authenticate() succeeds.
    print("Firebase UID:", auth.user_id)

    client = AquariteClient(auth)

    pools = await client.get_pools()
    for pool_id, pool_name in pools.items():
        data = await client.fetch_pool_data(pool_id)
        temperature = AquariteClient.get_value(data, "main.temperature")
        print(f"{pool_name}: {temperature}°C")

Writing values

set_value writes a single field; set_values writes several fields of the same command branch as one atomic command — useful when two fields must land together (e.g. a light's mode and status):

# Single field.
await client.set_value(pool_id, "filtration.mode", 1)

# Several fields, one command — sent together or not at all.
await client.set_values(pool_id, {"light.mode": 2, "light.status": 1})

Paths use dot notation ("hidro.cloration_enabled", "relays.relay1.info.onoff"). All paths passed to set_values must resolve to the same command branch — the same top-level key, and for deep 4+ segment paths, the same second-level key too — mixing branches raises ValueError. On a successful send, both methods immediately update the client's local pool-data cache, so the next command is built from the state the cloud just acknowledged rather than a stale Firestore snapshot.

Real-time updates

Subscribe with built-in token refresh and automatic reconnect (recommended). Callbacks run on the Firestore background thread — asyncio consumers should wrap them with loop.call_soon_threadsafe.

def on_pool_update(data):
    print("Pool updated:", data.get("main", {}).get("temperature"))

def on_pools_changed(pool_ids):
    print("User's pools:", pool_ids)

pool_sub = await client.subscribe_pool_resilient(pool_id, on_pool_update)
pools_sub = await client.subscribe_user_pools_resilient(on_pools_changed)

# ... later ...
await pool_sub.aclose()
await pools_sub.aclose()

Connection health

Both resilient subscriptions accept an optional on_health callback that reports connection-state transitions — on_health(False) when the connection is lost, on_health(True) once it is re-established. Useful for marking entities unavailable in a Home Assistant integration while the Firestore connection is down:

def on_health(healthy: bool) -> None:
    print("Connection healthy:" if healthy else "Connection LOST:", healthy)

pool_sub = await client.subscribe_pool_resilient(
    pool_id, on_pool_update, on_health=on_health
)
print(pool_sub.healthy)  # current connection state

on_health fires on transitions only, never for aclose(), and — unlike the data callback — is invoked from the event loop running the supervisor task, so no call_soon_threadsafe is needed. An exception raised by the callback is logged and never kills the supervisor.

Low-level subscriptions

If you want to own the connection lifecycle yourself, the raw watch handles are still available for both pool data and the user's pool list:

watch = await client.subscribe_pool(pool_id, on_pool_update)
# ... maintain token freshness, resubscribe on errors, etc. ...
watch.unsubscribe()

watch = await client.subscribe_user_pools(on_pools_changed)
watch.unsubscribe()

Historical stats & server clock

# Stored sample series (~30 days, ~10-minute granularity). Each point is
# {"field": <value>, "seconds": <utc_unix>}. Recognised types: ph, rx, temp,
# cl, cd, filtration, aux1..aux4 (plus hardware-conditional light/production/salt).
series = await client.get_pool_stats(pool_id, "ph", period=30)
print("pH samples:", len(series[0]))

# Clock-drift check against the Hayward backend (unauthenticated endpoint).
server_date = await client.get_server_date()
print("Server date:", server_date["date"])  # "YYMMDD"

Error handling

Every failure raises an AquariteError subclass, so a single except covers all of them:

from aioaquarite import AquariteError, AuthenticationError, CommandError, ConnectionError

try:
    await client.set_value(pool_id, "filtration.mode", 1)
except AuthenticationError:
    ...  # bad credentials, or refresh token rejected
except ConnectionError:
    ...  # transport failure or timeout talking to the Hayward cloud
except CommandError:
    ...  # the cloud function accepted the connection but rejected the command
except AquariteError:
    ...  # catch-all for anything else in the library

Development

git clone https://github.com/fdebrus/aioaquarite
cd aioaquarite
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
pip install pytest
python -m pytest tests/

Tests run automatically on every push and pull request via GitHub Actions.

License

MIT

Release files for aioaquarite 0.10.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for aioaquarite 0.10.0
File Size Uploaded
aioaquarite-0.10.0.tar.gz 34.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for aioaquarite 0.10.0
File Interpreter ABI Platform
aioaquarite-0.10.0-py3-none-any.whl Python 3 none any Details

Total release size: 55.3 kB

Release files / aioaquarite-0.10.0.tar.gz

Download URL aioaquarite-0.10.0.tar.gz
Size 34.0 kB
Tags Source
SHA-256 checksum
How to use checksums
378ec359e3c13f81a1efcb294a0f3319ea70eaa1a1b3a4013ee2506c3c1be89f
BLAKE2b-256 checksum
How to use checksums
d3c4545a8d34a5f4f9f3685826ea49cc8c3790a3280a2d4fac27d25b26924c03
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 30, 2026.

Transparency log

Release files / aioaquarite-0.10.0-py3-none-any.whl

Download URL aioaquarite-0.10.0-py3-none-any.whl
Size 21.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bb94b5a559630e14b93adfa9e34c8f4a1ba6cbdd269570b63d11054b2d0d6e2b
BLAKE2b-256 checksum
How to use checksums
cafdb26d6d8b2326b669e8ce835021ee2e6f2f5555ffc805d587316f30674073
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 30, 2026.

Transparency log

Release history Release notifications | RSS feed

0.11.0

2 release files

This release

0.10.0 This release

2 release files

0.9.2

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page