Python library for Watlow temperature controllers (Standard Bus and Modbus RTU over serial).
Project description
watlowlib
Async-first Python driver for Watlow temperature
controllers over RS-232 / EIA-485. Speaks both wire protocols Watlow
controllers expose — Standard Bus (BACnet MS/TP outer framing + a
small Watlow attribute service) and Modbus RTU (via the in-house
anymodbus) — behind a
single semantic Controller API that decodes to the same typed
Reading either way.
Built as a sibling to
alicatlib and
sartoriuslib: the
same async core, sync facade, multi-device manager, fake transport,
acquisition helpers, and pluggable sinks.
Status: alpha. Both wire protocols, the
Controllerfacade,WatlowManager, streaming, all sinks, the sync facade, and every CLI are implemented and covered by 466 tests acrossasyncio,asyncio+uvloop, andtrio. The core has been exercised against a live EZ-ZONE PM3 (Standard Bus); the Modbus RTU side has full codec / client / integration coverage but limited bench mileage. Expect API churn until 1.0. See docs/design.md for the architectural reference.
Goals
- One protocol-neutral API.
Controller.read_pv(),read_setpoint(),set_setpoint(),read_parameter()— the same calls work over Standard Bus and Modbus RTU, decoding to identicalReading/LoopState/DeviceInfomodels. - Auto-detect.
open_device(..., protocol=ProtocolKind.AUTO)does Standard Bus probe → Modbus RTU probe → fail clearly. Read-only by construction; never sweeps opcodes or guesses bauds. - Cross-protocol parameter registry. The EZ-ZONE register list
carries both the Standard Bus selector (
class/member/instance) and the Modbus register addresses for every parameter — soread_parameter("setpoint")lowers to either protocol from one shared registry. - Typed end to end.
TemperatureUnit.F,Capability.MULTI_LOOP, frozen-dataclass responses,py.typed,mypy --strictclean. - Typed errors.
WatlowErrorroot with structuredErrorContext; every Standard Bus error code (0x81/0x83/0x84) and every Modbus exception (0x01–0x0B) maps to a distinct exception. - Safety gates.
PERSISTENT(RWE) andDANGEROUS(RWES / calibration / baud-change) operations requireconfirm=True. - Multi-device.
WatlowManagerruns many controllers concurrently — same-port requests serialize, different ports run in parallel. - Acquisition built in.
record(...)drives one or many devices on an absolute-target cadence into pluggable sinks:InMemorySink,CsvSink,JsonlSink,SqliteSinkin core;ParquetSinkandPostgresSinkbehind extras. - Swappable transports.
SerialTransportfor hardware,FakeTransportfor tests, fixture-backed transports for regression goldens. - Sync or async. Async core on
anyio; complete sync facade atwatlowlib.syncvia a blocking portal — every async method has a sync parity. - CLI tooling.
watlow-read,watlow-discover,watlow-raw,watlow-decode,watlow-configure, and thewatlow-diagreverse-engineering namespace. - Lean core.
pip install watlowlibpulls inanyio,anyserial, andanymodbus— nothing else.
Install
pip install watlowlib
# optional sinks
pip install 'watlowlib[parquet]' # ParquetSink (pyarrow)
pip install 'watlowlib[postgres]' # PostgresSink (asyncpg)
Requires Python 3.13+. Linux, macOS, BSD, and Windows are supported
via anyserial. On Linux, the
user running watlow-* needs read/write access to the serial device —
usually by joining the dialout group.
Quickstart (async)
import anyio
from watlowlib import open_device, ProtocolKind
async def main() -> None:
async with await open_device(
"/dev/ttyUSB0",
protocol=ProtocolKind.AUTO,
address=1,
) as ctl:
info = await ctl.identify()
print(info.model, info.part_number, info.firmware)
pv = await ctl.read_pv()
print(pv.value, pv.unit)
await ctl.set_setpoint(75.0, confirm=True)
anyio.run(main)
Older Watlow controllers (Series 96, F4, early EZ-ZONE) ship from the
factory in Standard Bus; on those models Modbus RTU is the
front-panel-opt-in mode. Newer EZ-ZONE firmware can ship in either mode
depending on configuration. The ProtocolKind.AUTO detector tries
Standard Bus first then Modbus RTU.
Quickstart (sync)
from watlowlib.sync import Watlow
with Watlow.open("/dev/ttyUSB0", address=1) as ctl:
print(ctl.read_pv())
ctl.set_setpoint(75.0, confirm=True)
Multi-device acquisition
import anyio
from watlowlib import WatlowManager
from watlowlib.streaming import record
from watlowlib.sinks import CsvSink, pipe
async def main() -> None:
async with WatlowManager() as mgr:
await mgr.add("oven", "/dev/ttyUSB0", address=1)
await mgr.add("furnace", "/dev/ttyUSB0", address=2)
async with (
record(mgr, rate_hz=2, duration=3600) as stream,
CsvSink("run.csv") as sink,
):
await pipe(stream, sink)
anyio.run(main)
The recorder runs on an absolute target cadence (drift-free), batches
samples per tick, and reports send/receive timing on every Sample.
Command-line tools
watlow-discover /dev/ttyUSB0 # probe + identify
watlow-read /dev/ttyUSB0 --protocol auto # one decoded poll
watlow-decode --stdbus 55 FF 06 00 10 ... # offline frame decode
watlow-raw /dev/ttyUSB0 --param 4001 --confirm # raw escape hatch
watlow-configure switch-protocol /dev/ttyUSB0 --confirm
watlow-diag snapshot /dev/ttyUSB0 --out diag.json # reverse-engineering aids
All CLIs accept --fixture FILE to drive a scripted FakeTransport, so
end-to-end tests and demos work without hardware.
Documentation
Full docs live at https://GraysonBellamy.github.io/watlowlib/. Useful entry points:
- Async quickstart / Sync quickstart
- Controllers and capabilities
- Commands and safety tiers / Safety
- Parameter registry
- Streaming and acquisition / Logging
- Standard Bus reference / Standard Bus findings / Modbus RTU mapping
- Testing —
FakeTransport, fixtures, hardware tiers - Architecture (design doc)
Development
uv for env and lock management,
hatchling + hatch-vcs for builds, ruff for format and lint, mypy --strict and pyright for types, AnyIO's pytest plugin for the test
suite (parametrised across asyncio, asyncio+uvloop, and trio).
uv sync --all-extras --dev
uv run pre-commit install
uv run pytest
uv run ruff format --check .
uv run ruff check .
uv run mypy
Hardware tests are gated behind WATLOWLIB_ENABLE_STATEFUL_TESTS=1 and
WATLOWLIB_ENABLE_DESTRUCTIVE_TESTS=1 and require a connected
controller.
See CONTRIBUTING.md for the workflow and SECURITY.md for the disclosure policy.
License
MIT. 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 watlowlib-0.1.0.tar.gz.
File metadata
- Download URL: watlowlib-0.1.0.tar.gz
- Upload date:
- Size: 334.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a75f060d695c188676928cad908b4433d939b4ea33e087f02de8e45d1bec1f88
|
|
| MD5 |
be2948907302b04b130a27b084062abd
|
|
| BLAKE2b-256 |
de2a37c2a05ec9159e2aea55211519ee2536e27fd028e80ed2c4fb3b6446418f
|
Provenance
The following attestation bundles were made for watlowlib-0.1.0.tar.gz:
Publisher:
release.yml on GraysonBellamy/watlowlib
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
watlowlib-0.1.0.tar.gz -
Subject digest:
a75f060d695c188676928cad908b4433d939b4ea33e087f02de8e45d1bec1f88 - Sigstore transparency entry: 1406051651
- Sigstore integration time:
-
Permalink:
GraysonBellamy/watlowlib@450b8cede04ce5409a0050d8f5f5e5ea544ad693 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/GraysonBellamy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@450b8cede04ce5409a0050d8f5f5e5ea544ad693 -
Trigger Event:
release
-
Statement type:
File details
Details for the file watlowlib-0.1.0-py3-none-any.whl.
File metadata
- Download URL: watlowlib-0.1.0-py3-none-any.whl
- Upload date:
- Size: 302.1 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 |
98db819328e44c873ec3d045f308be342a0f864c99a5fcdf71b6d538abfa957d
|
|
| MD5 |
c0e6a152dd16b2d8d76968b1916ee441
|
|
| BLAKE2b-256 |
d139fc930818e6abf0d1f74ac00e10da274d6e5b6e9f1e8a0ce66f98470d0d4d
|
Provenance
The following attestation bundles were made for watlowlib-0.1.0-py3-none-any.whl:
Publisher:
release.yml on GraysonBellamy/watlowlib
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
watlowlib-0.1.0-py3-none-any.whl -
Subject digest:
98db819328e44c873ec3d045f308be342a0f864c99a5fcdf71b6d538abfa957d - Sigstore transparency entry: 1406051721
- Sigstore integration time:
-
Permalink:
GraysonBellamy/watlowlib@450b8cede04ce5409a0050d8f5f5e5ea544ad693 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/GraysonBellamy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@450b8cede04ce5409a0050d8f5f5e5ea544ad693 -
Trigger Event:
release
-
Statement type: