Async Python client for Sugar Valley NeoPool / VistaPool / Hidrolife Modbus pool controllers
Project description
neopool-modbus
Async Python client for NeoPool-based pool controllers connected via Modbus TCP. NeoPool is a control system originally developed by the Spanish company Sugar Valley (acquired by Hayward in 2016), sold under many brand names and product lines worldwide.
Supported device models (Sugar Valley / Hayward product lines): Hidrolife • Aquascenic • Oxilife • Bionet • Hidroniser • UVScenic • Station • Aquarite
Distributed by (vendors selling NeoPool-based hardware): Hayward • Brilix (Albixon) • Bayrol • Certikin • Poolstar • GrupAquadirect • Pentair • ProducPool • Pool Technologie • Kripsol
Note: VistaPool is the name of Hayward's mobile/web app for cloud-based pool management. This library communicates locally via Modbus TCP — it does not require or use the VistaPool app or any cloud service.
This library is the communication layer extracted from the
Home Assistant neopool integration
and is suitable for any async Python project — Home Assistant integrations,
scripts, dashboards, or custom automation.
Installation
pip install neopool-modbus
Requires Python 3.13+ and pymodbus>=3.10.0 (installed transitively).
Quick start
import asyncio
from neopool_modbus import NeoPoolModbusClient
async def main() -> None:
client = NeoPoolModbusClient(
{"host": "192.168.1.42", "port": 502, "slave_id": 1}
)
try:
data = await client.async_read_all()
# Keys are the NeoPool register names defined by Sugar Valley
# (mirrored from Tasmota's xsns_83_neopool.ino driver);
# values are decoded into native Python types.
print(f"pH: {data['MBF_MEASURE_PH']}") # e.g. 7.42
print(f"Temperature: {data['MBF_MEASURE_TEMPERATURE']} °C") # e.g. 27.3
print(f"Hydrolysis: {data['MBF_HIDRO_CURRENT']}") # e.g. 6.5
finally:
await client.close()
asyncio.run(main())
The client is lazy — it opens the TCP connection on first use and reuses it
across calls; close() releases the socket and resets retry/backoff state.
Reading individual registers
For one-off reads by address, async_read_register(address, count=1) picks
the correct Modbus function code automatically: Read Input Registers
(FC 0x04) for any address on the 0x01 page (MEASURE) and Read Holding
Registers (FC 0x03) elsewhere.
from neopool_modbus import NeoPoolModbusClient
client = NeoPoolModbusClient({"host": "192.168.1.42"})
# Single register — pH level (raw u16, divide by 100 for pH 7.20)
ph_raw = (await client.async_read_register(0x0102))[0]
# Multi-register read (1-31; the firmware refuses larger requests).
# Combine the two halves of a 32-bit cell-runtime counter:
low, high = await client.async_read_register(0x0208, count=2)
partial_seconds = (high << 16) | low
The method validates the request before touching the wire and raises
ValueError for out-of-range addresses, counts that exceed
MAX_REGISTERS_PER_READ (31), or ranges that would either cross the
input/holding namespace boundary or extend past the 16-bit address space.
Public API
from neopool_modbus import (
NeoPoolModbusClient,
NeoPoolError,
NeoPoolConnectionError,
NeoPoolModbusError,
NeoPoolTimeoutError,
async_probe_serial,
)
from neopool_modbus.registers import (
CLEAR_EEPROM_REGISTER,
COMMAND_REGISTERS,
COPY_TO_RTC_REGISTER,
DEFAULT_MODBUS_FRAMER,
EEPROM_SAVE_REGISTER,
ESCAPE_REGISTER,
EXEC_REGISTER,
HEATING_SETPOINT_REGISTER,
INPUT_REGISTER_RANGES,
INTELLIGENT_SETPOINT_REGISTER,
MANUAL_FILTRATION_REGISTER,
MAX_REGISTERS_PER_READ,
RESET_USER_COUNTERS_REGISTER,
STOP_ALL_MODULES_REGISTER,
TIMER_BLOCKS,
is_input_register,
is_valid_relay_gpio,
)
from neopool_modbus.decoders import (
parse_timer_block,
build_timer_block,
hhmm_to_seconds,
seconds_to_hhmm,
get_machine_name,
is_hydrolysis_in_percent,
# ... see neopool_modbus.decoders for the full list
)
from neopool_modbus.status_mask import (
decode_relay_state,
decode_named_relay_states,
decode_uv_lamp_state,
decode_hidro_status_bits,
decode_ion_status_bits,
decode_ph_rx_cl_cd_status_bits,
)
All client methods translate underlying pymodbus exceptions into the
NeoPoolError hierarchy at the library boundary, so callers never need
to import pymodbus to catch errors:
| Class | Raised when |
|---|---|
NeoPoolConnectionError |
TCP connect fails, returned False, or the client is in its post-failure backoff |
NeoPoolTimeoutError |
Connect, read, or write times out (asyncio.TimeoutError) |
NeoPoolModbusError |
A read returns a Modbus exception response (isError() true), or async_write_aux_relay / one of the timer write follow-ups returns isError() |
NeoPoolError |
Common base; catch this to handle any of the above |
⚠️
NeoPoolModbusClient.async_write_register()is the exception to the table above: it returnsNone(rather than raising) onisError()so existing callers in the Home Assistant integration keep working. A future major release will tighten this to raiseNeoPoolModbusErrorfor consistency.
from neopool_modbus import NeoPoolError, NeoPoolModbusClient
client = NeoPoolModbusClient({"host": "192.168.1.42"})
try:
data = await client.async_read_all()
except NeoPoolError as exc:
# exc.__cause__ is the original pymodbus / asyncio exception, if any.
print(f"NeoPool read failed: {exc}")
ValueError is still raised directly for programmer errors such as an
out-of-range AUX relay index — those are not transport failures.
Features
- Async I/O on top of
pymodbus.AsyncModbusTcpClient - Batched register reads — one round-trip per protocol page, with notification-bit-driven cache invalidation so unchanged pages skip the read
- Public read-by-address API (
async_read_register) that automatically picks Read Input vs Read Holding based on the address - Exponential connection retry with bounded backoff
- Write-and-verify cycle for configuration registers, with auto-clearing
command registers (
COMMAND_REGISTERS) excluded from verification - Capability detection (hydrolysis, pH, Redox, chlorine, conductivity, ION)
- Strict type hints (
py.typed), 100 % unit-test coverage
Logging
The library uses a single logger named neopool_modbus. Enable it like any
other Python logger:
import logging
logging.getLogger("neopool_modbus").setLevel(logging.DEBUG)
Home Assistant users can flip the integration's "Enable debug logging" toggle
in the UI; the integration's manifest.json lists neopool_modbus so the
toggle covers the library too.
Based On
- Tasmota NeoPool driver — implements the NeoPool Modbus register protocol originally documented by Sugar Valley
- NeoPool Control System MODBUS Register description — a Markdown transcription of the official Modbus register documentation by Sugar Valley (see
docs/modbus-registers.md)
Disclaimer
This library is provided "AS IS" and without any warranty or guarantee of any kind. The author takes no responsibility for any damage, loss, or malfunction resulting from the use or misuse of this code. Use at your own risk.
This project is not affiliated with or endorsed by Sugar Valley, Hayward, or any other pool equipment manufacturer or distributor.
"VistaPool" is a trademark of Hayward Industries, Inc. This library communicates locally via Modbus and does not use the VistaPool cloud service.
License
Apache 2.0 — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file neopool_modbus-2.1.0.tar.gz.
File metadata
- Download URL: neopool_modbus-2.1.0.tar.gz
- Upload date:
- Size: 64.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4babe5ecb57aff58ed8e88f543c07b2ef1a1a4106efed58d7c54d116916e8faa
|
|
| MD5 |
91cd42cd2a3cdd76d224495fe2abcd12
|
|
| BLAKE2b-256 |
a3c3d43a6358af01bdfa6ca4e9af17deae9c0ee646b105fed5afa1ee3a2bece0
|
Provenance
The following attestation bundles were made for neopool_modbus-2.1.0.tar.gz:
Publisher:
release.yaml on svasek/python-neopool-modbus
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
neopool_modbus-2.1.0.tar.gz -
Subject digest:
4babe5ecb57aff58ed8e88f543c07b2ef1a1a4106efed58d7c54d116916e8faa - Sigstore transparency entry: 1810627940
- Sigstore integration time:
-
Permalink:
svasek/python-neopool-modbus@2be9b7e144664bec02551086c843aaf22c67aa5a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/svasek
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@2be9b7e144664bec02551086c843aaf22c67aa5a -
Trigger Event:
push
-
Statement type:
File details
Details for the file neopool_modbus-2.1.0-py3-none-any.whl.
File metadata
- Download URL: neopool_modbus-2.1.0-py3-none-any.whl
- Upload date:
- Size: 43.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f1a3ffd08115fa6134c4740cabe066942fdaba354a3fee9e86ba2d875fc1fff3
|
|
| MD5 |
2ce5f9b085511447a4ef13b2e799f8e8
|
|
| BLAKE2b-256 |
e0adadfe227f49f7b2925a926f2d19819672ab823f998006ca690e6376ff5b13
|
Provenance
The following attestation bundles were made for neopool_modbus-2.1.0-py3-none-any.whl:
Publisher:
release.yaml on svasek/python-neopool-modbus
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
neopool_modbus-2.1.0-py3-none-any.whl -
Subject digest:
f1a3ffd08115fa6134c4740cabe066942fdaba354a3fee9e86ba2d875fc1fff3 - Sigstore transparency entry: 1810627946
- Sigstore integration time:
-
Permalink:
svasek/python-neopool-modbus@2be9b7e144664bec02551086c843aaf22c67aa5a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/svasek
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@2be9b7e144664bec02551086c843aaf22c67aa5a -
Trigger Event:
push
-
Statement type: