Skip to main content

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 Controller facade, WatlowManager, streaming, all sinks, the sync facade, and every CLI are implemented and covered by 579 passing tests across asyncio, asyncio+uvloop, and trio. The core has been exercised against a live EZ-ZONE PM3 (Standard Bus), a Series SD over Modbus RTU, and a Series F4P (the controller inside an ISDC IR-301 blackbody source) over Modbus RTU; the Modbus RTU side has codec / client / integration coverage plus captured PM3, SD, and F4P fixture goldens. 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 identical Reading / LoopState / DeviceInfo models.
  • 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 — so read_parameter("setpoint") lowers to either protocol from one shared registry.
  • Typed end to end. Unit.FAHRENHEIT, Capability.HAS_COOLING, frozen-dataclass responses, py.typed, mypy --strict clean.
  • Typed errors. WatlowError root with structured ErrorContext; every Standard Bus error code (0x81 / 0x83 / 0x84) and every Modbus exception (0x01–0x0B) maps to a distinct exception.
  • Safety gates. Every PERSISTENT write — including setpoint, PID, baud, address, and protocol-mode changes — requires confirm=True.
  • Multi-device. WatlowManager runs 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, SqliteSink in core; ParquetSink and PostgresSink behind extras.
  • Swappable transports. SerialTransport for hardware, FakeTransport for tests, fixture-backed transports for regression goldens.
  • Sync or async. Async core on anyio; complete sync facade at watlowlib.sync via a blocking portal — every async method has a sync parity.
  • CLI tooling. watlow-read, watlow-discover, watlow-raw, watlow-decode, watlow-configure, and the watlow-diag reverse-engineering namespace.
  • Lean core. pip install watlowlib pulls in anyio, anyserial, and anymodbus — 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,
                parameters=["process_value", "setpoint"],
                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 --port /dev/ttyUSB0 --protocol both # probe + identify
watlow-read --port /dev/ttyUSB0 --protocol auto -p process_value
watlow-read --port COM8 --profile f4p --address 10 -p process_value
watlow-decode "55 FF 06 00 10 ..."                  # offline Std Bus decode
watlow-raw --port /dev/ttyUSB0 stdbus --service read --class 4 --member 1
watlow-configure change-protocol-mode /dev/ttyUSB0 --target modbus_rtu --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://watlowlib.graysonbellamy.dev/. Useful entry points:

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.

Metadata

Release files for watlowlib 0.8.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 watlowlib 0.8.0
File Size Uploaded
watlowlib-0.8.0.tar.gz 419.6 kB Details

Built distribution (wheel)

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

Total release size: 771.7 kB

Release files / watlowlib-0.8.0.tar.gz

Download URL watlowlib-0.8.0.tar.gz
Size 419.6 kB
Tags Source
SHA-256 checksum
How to use checksums
f775003b70f3097bf5377331cb68263b3f1d30e4220c3af0b924c6412a9a49e0
BLAKE2b-256 checksum
How to use checksums
abbc1bb750bf9b29b29e21e7160cdba2c574b3833ec33c7cd9f115b25ad0de35
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 Oct 11, 2026.

Transparency log

Release files / watlowlib-0.8.0-py3-none-any.whl

Download URL watlowlib-0.8.0-py3-none-any.whl
Size 352.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ff2758c5cd6f3200e4e408130ce0b3c69d6adad8d4ad0da18ee4eacc3b55e6bb
BLAKE2b-256 checksum
How to use checksums
c245f76c82a273856baec4a98e5e6436aad42c68e21822f21ffe92cacb7f43b4
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 Oct 11, 2026.

Transparency log

Release history Release notifications | RSS feed

0.8.1

2 release files

This release

0.8.0 This release

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.0

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