watlowlib.devices¶
The Controller facade, Session, typed dataclasses (Reading,
DeviceInfo, PartNumber, AlarmState, LoopState,
DiscoveryResult, …), ControllerFamily, Capability, SafetyTier,
Availability, open_device, open_controller, and discovery
helpers. See Controllers.
Public surface¶
watlowlib.devices ¶
Device facade — :class:Controller, :class:Session, and dataclasses.
The facade is the public surface; everything else
(:mod:watlowlib.protocol, :mod:watlowlib.commands,
:mod:watlowlib.registry) is implementation detail callers don't
have to import.
AlarmState
dataclass
¶
Decoded alarm bits for one loop.
Availability ¶
Bases: StrEnum
Per-command session state.
Sticky for the session: once a command transitions to
:attr:UNSUPPORTED, the session short-circuits subsequent
invocations with a typed error pre-I/O. The transition table
lives in docs/design.md §5b.
Capability ¶
Bases: Flag
Coarse hardware capability bits.
Bits are derived from a decoded part number when one is available
(see :func:watlowlib.registry.families.capabilities_for_part_number)
and fall back to a per-family prior otherwise. The session widens
the set at runtime when a command succeeds against a parameter
that proves the capability.
The vocabulary is small on purpose — most Watlow gating is by
:class:watlowlib.registry.families.ControllerFamily and by
:attr:watlowlib.registry.parameters.ParameterSpec.parameter_id,
not by per-feature bits. New bits are added when captured family
behaviour requires them.
Controller ¶
Async facade for a single Watlow controller.
Source code in src/watlowlib/devices/controller.py
capabilities
property
¶
Cached SKU capabilities (set after :meth:identify).
None pre-identify so capability-gated operations behave
permissively until the part number is captured. After
:meth:identify, callers can branch on
:attr:Capability.HAS_COOLING etc. without re-issuing
identify.
loops
property
¶
Cached loop count (set after :meth:identify).
None until the device's part number has been decoded;
:meth:loop accepts any 1-indexed value while loops is
None and falls back to per-spec validation at the first
wire call. After :meth:identify, loops reflects the
decoded value.
aclose
async
¶
Close the underlying transport and dispose the protocol client.
Source code in src/watlowlib/devices/controller.py
identify
async
¶
Read the identity parameters and return a :class:DeviceInfo.
Reads (in order): part number (1009), hardware id (1001),
firmware id (1002), serial number. Missing secondary fields
stay None and the result's :attr:DeviceInfo.health is
promoted from :attr:DeviceHealth.OK to
:attr:DeviceHealth.PARTIAL. If the part-number read itself
fails, the result's health is :attr:DeviceHealth.FAILED and
capability decoding is skipped (the family prior still
applies).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timeout
|
float | None
|
Per-read timeout override. |
None
|
strict
|
bool
|
If |
False
|
query_configured_protocol
|
bool
|
If |
False
|
Raises:
| Type | Description |
|---|---|
WatlowError
|
When |
Source code in src/watlowlib/devices/controller.py
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 | |
loop ¶
Return a sub-facade bound to loop n (1-indexed).
n is validated eagerly when :attr:loops is known,
otherwise per-spec max_instance validation kicks in at the
first wire call. Multi-loop access is the public way to reach
loop 2 on dual-loop devices —
:meth:Controller.read_pv defaults to instance=1.
Source code in src/watlowlib/devices/controller.py
poll
async
¶
Read every (parameter × instance) and return them as :class:Sample\ s.
Satisfies the :class:watlowlib.streaming.PollSource Protocol so
a solo :class:Controller can drive :func:watlowlib.streaming.record
directly without a manager. names is accepted for Protocol
compatibility but ignored — a Controller has only one device.
Failed reads are dropped from the returned list and logged at WARN. The recorder treats absence as "drop this row from the batch" and continues with the next tick.
Source code in src/watlowlib/devices/controller.py
read_parameter
async
¶
Read any registry parameter.
instance=1 is the default for single-loop devices and the
first loop / channel on multi-loop devices.
Source code in src/watlowlib/devices/controller.py
read_pv
async
¶
Read the process value for instance (loop number, 1-indexed).
Source code in src/watlowlib/devices/controller.py
read_setpoint
async
¶
Read the active setpoint for instance.
Source code in src/watlowlib/devices/controller.py
set_setpoint
async
¶
Write the setpoint and return the device-echoed value as a :class:Reading.
Setpoint is RWES — pass confirm=True to acknowledge the
EEPROM write. The returned reading is the device's echo of
the value it accepted.
Source code in src/watlowlib/devices/controller.py
write_parameter
async
¶
Write any registry parameter.
Persistent (RWE / RWES) writes require confirm=True;
the session raises :class:WatlowConfirmationRequiredError
before any I/O if the gate is missing.
Source code in src/watlowlib/devices/controller.py
ControllerFamily ¶
Bases: StrEnum
Watlow controller family discriminator.
Membership here is advisory — :class:watlowlib.devices.session.Session
treats family hints as priors, not gates, unless the session was
opened with strict=True. See docs/design.md §5b.
ControllerLoop ¶
A view over one control loop on a :class:Controller.
Construct via :meth:Controller.loop; never instantiated
directly by user code. The sub-facade lives only as long as the
parent controller's session — closing the controller is the only
cleanup needed.
Source code in src/watlowlib/devices/loop.py
read_alarms
async
¶
Read the alarm word for this loop.
Currently raises :class:watlowlib.errors.WatlowProtocolUnsupportedError —
see :func:watlowlib.commands.alarms.read_alarms for why the
decoder is not yet wired up.
Source code in src/watlowlib/devices/loop.py
read_output
async
¶
read_pid
async
¶
Read every PID gain for this loop. Missing gains return None.
Cool-side gains (cool_proportional_band, dead_band)
are skipped when the controller's identified capabilities
lack :attr:Capability.HAS_COOLING (e.g. PM output_2 ==
'A'). Pre-identify, the gate is permissive.
Source code in src/watlowlib/devices/loop.py
read_pv
async
¶
read_setpoint
async
¶
set_setpoint
async
¶
Write this loop's setpoint (RWES → confirm=True required).
Source code in src/watlowlib/devices/loop.py
write_pid
async
¶
Write the supplied gains for this loop.
Persistent — passing confirm=True is required. Fields
left None on gains skip the wire entirely. Setting a
cool-side field on a controller without
:attr:Capability.HAS_COOLING raises
:class:watlowlib.errors.WatlowConfigurationError.
Source code in src/watlowlib/devices/loop.py
DeviceInfo
dataclass
¶
DeviceInfo(
part_number,
hardware_id,
firmware_id,
serial_number,
family,
protocol,
address,
capabilities,
serial_settings,
loops,
health=DeviceHealth.OK,
configured_protocol=None,
)
Identity + connection metadata for an open controller.
Returned by :meth:Controller.identify. Capabilities are decoded
from the part number when one is captured (see
:func:watlowlib.registry.families.capabilities_for_part_number)
and OR-ed with the family prior; unobserved bits stay zero rather
than being guessed.
protocol is the wire protocol the host is currently talking;
configured_protocol is what the device's persistent EEPROM
parameter (PM 17009) reports. They normally match, but when they
diverge the helper :attr:protocol_mismatch flags it — useful
for catching SKU/firmware combinations where the user wrote a new
protocol but the runtime stack didn't pick it up (e.g. comms
position-8 = 'A', no Modbus stack present even though 17009 reads
1057).
protocol_mismatch
property
¶
True when EEPROM says one protocol and we're talking another.
Always False when :attr:configured_protocol is None
(i.e. identify did not query parameter 17009).
DiscoveryResult
dataclass
¶
One row from the discovery sweep.
LoopState
dataclass
¶
Snapshot of one loop. Composed from several reads.
ParameterEntry
dataclass
¶
Generic registry-driven read/write result.
Returned by :data:watlowlib.commands.READ_PARAMETER and
:data:watlowlib.commands.WRITE_PARAMETER. The
:class:Controller translates an entry into a :class:Reading /
:class:PartNumber / etc. when the public API guarantees a
richer shape.
PartNumber
dataclass
¶
Parsed part-number string returned by read_part_number.
Per-family digit decoding is contributed by
:mod:watlowlib.registry.families. Decoded fragments live in
:attr:details as a free-form mapping so each family can populate
only what its ordering format defines, and so adding fragments to
the PM decoder later is non-breaking.
The EZ-ZONE PM decoder populates case size, control type, power
input, three output codes, and options string. Other families fall
through to a stub: only :attr:family is set, and :attr:details
is empty.
Reading
dataclass
¶
A single timestamped value from the controller.
protocol is set by the variant decoder, not by the facade —
it reflects which wire protocol produced the value (per
docs/design.md invariant 7).
SafetyTier ¶
Bases: IntEnum
How dangerous a command is to invoke.
READ_ONLY(R) — no state change.STATEFUL— runtime state change but not EEPROM-backed. Reserved for commands like "start autotune"; no PM parameter maps here today, but the tier exists so future commands have a place to live.PERSISTENT(RW / RWE / RWES) — EEPROM-backed; requiresconfirm=Trueat the facade.
Session ¶
Owns availability cache, gates, and the dispatch loop.
A :class:Session is bound to exactly one :class:ProtocolClient
for its lifetime — one protocol per port (invariant 1).
Source code in src/watlowlib/devices/session.py
client
property
¶
The bound protocol client.
Exposed for the watlow-raw escape hatch and for diagnostics
that need to issue an unframed wire op outside the registry.
Callers must acquire :attr:ProtocolClient.lock before
:meth:ProtocolClient.execute to honour the per-port
serialization invariant, and must pass this session's
:attr:address (or another concrete address for multi-drop
diagnostics) to execute.
registry
property
¶
Parameter registry bound to this session.
Exposed for the streaming layer so polling code can resolve a
name / id to a :class:ParameterSpec without an extra import
of the module-level :data:PARAMETERS.
availability ¶
dispose ¶
execute
async
¶
Dispatch command with request and return the typed response.
Source code in src/watlowlib/devices/session.py
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 | |
classify_family ¶
Return the :class:ControllerFamily for a part-number string.
Only the leading family discriminator is parsed; per-family digit
decoding is in :func:decode_part_number.
Source code in src/watlowlib/registry/families.py
open_controller
async
¶
open_controller(
transport,
*,
protocol,
address,
serial_settings,
family=ControllerFamily.UNKNOWN,
)
Build a :class:Controller over an existing :class:Transport.
Tests use this to drive the facade through a
:class:watlowlib.transport.fake.FakeTransport. Production code
uses :func:open_device.
Source code in src/watlowlib/devices/factory.py
open_device
async
¶
Open a controller on a serial port.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
port
|
str
|
Serial-port path ( |
required |
protocol
|
ProtocolKind
|
Wire protocol. |
STDBUS
|
address
|
int
|
Bus address. Std Bus accepts |
1
|
serial_settings
|
SerialSettings | None
|
Optional override. Default is 38400 8-N-1,
the EZ-ZONE PM Standard Bus factory setting; |
None
|
Returns:
| Type | Description |
|---|---|
Controller
|
An opened :class: |
Controller
|
detector held the transport open after a successful probe), |
Controller
|
otherwise an unopened :class: |
Controller
|
async context manager. |
Raises:
| Type | Description |
|---|---|
WatlowConfigurationError
|
|
WatlowProtocolUnsupportedError
|
|
Source code in src/watlowlib/devices/factory.py
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | |
sweep_modbus
async
¶
sweep_modbus(
port,
*,
addresses=DEFAULT_MODBUS_RANGE,
serial_settings=None,
timeout_s=_DEFAULT_PROBE_TIMEOUT_S,
)
Yield one :class:DiscoveryResult per Modbus address probed.
Same shape as :func:sweep_stdbus — sequential, with the bus
transport opened once and reused across every slave address. The
Modbus driver multiplexes slaves over a single open handle so no
per-address transport churn is incurred.
Source code in src/watlowlib/devices/discovery.py
sweep_stdbus
async
¶
sweep_stdbus(
port,
*,
addresses=DEFAULT_STDBUS_RANGE,
serial_settings=None,
timeout_s=_DEFAULT_PROBE_TIMEOUT_S,
)
Yield one :class:DiscoveryResult per Std Bus address probed.
Opens the serial transport once, walks addresses sequentially
against the same open handle, then closes. Silent rows carry a
populated :attr:DiscoveryResult.error of type
:class:watlowlib.errors.WatlowTimeoutError (or
:class:watlowlib.errors.WatlowTransportError for framing issues)
so callers can distinguish "device absent" from "address never
tried".
timeout_s bounds every underlying parameter read; the default
keeps a 16-address silent sweep under five seconds.
Source code in src/watlowlib/devices/discovery.py
Controller¶
watlowlib.devices.controller ¶
The :class:Controller facade — public API for one device.
Single-device surface:
- :meth:
identify - :meth:
read_pv/ :meth:read_setpoint/ :meth:set_setpoint - :meth:
read_parameter/ :meth:write_parameter - :meth:
loop(multi-loop access), PID, alarms
Lifecycle is async-context-manager: async with await open_device(...)
opens the transport on __aenter__ and disposes the protocol client
+ closes the transport on __aexit__.
Controller ¶
Async facade for a single Watlow controller.
Source code in src/watlowlib/devices/controller.py
capabilities
property
¶
Cached SKU capabilities (set after :meth:identify).
None pre-identify so capability-gated operations behave
permissively until the part number is captured. After
:meth:identify, callers can branch on
:attr:Capability.HAS_COOLING etc. without re-issuing
identify.
loops
property
¶
Cached loop count (set after :meth:identify).
None until the device's part number has been decoded;
:meth:loop accepts any 1-indexed value while loops is
None and falls back to per-spec validation at the first
wire call. After :meth:identify, loops reflects the
decoded value.
aclose
async
¶
Close the underlying transport and dispose the protocol client.
Source code in src/watlowlib/devices/controller.py
identify
async
¶
Read the identity parameters and return a :class:DeviceInfo.
Reads (in order): part number (1009), hardware id (1001),
firmware id (1002), serial number. Missing secondary fields
stay None and the result's :attr:DeviceInfo.health is
promoted from :attr:DeviceHealth.OK to
:attr:DeviceHealth.PARTIAL. If the part-number read itself
fails, the result's health is :attr:DeviceHealth.FAILED and
capability decoding is skipped (the family prior still
applies).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timeout
|
float | None
|
Per-read timeout override. |
None
|
strict
|
bool
|
If |
False
|
query_configured_protocol
|
bool
|
If |
False
|
Raises:
| Type | Description |
|---|---|
WatlowError
|
When |
Source code in src/watlowlib/devices/controller.py
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 | |
loop ¶
Return a sub-facade bound to loop n (1-indexed).
n is validated eagerly when :attr:loops is known,
otherwise per-spec max_instance validation kicks in at the
first wire call. Multi-loop access is the public way to reach
loop 2 on dual-loop devices —
:meth:Controller.read_pv defaults to instance=1.
Source code in src/watlowlib/devices/controller.py
poll
async
¶
Read every (parameter × instance) and return them as :class:Sample\ s.
Satisfies the :class:watlowlib.streaming.PollSource Protocol so
a solo :class:Controller can drive :func:watlowlib.streaming.record
directly without a manager. names is accepted for Protocol
compatibility but ignored — a Controller has only one device.
Failed reads are dropped from the returned list and logged at WARN. The recorder treats absence as "drop this row from the batch" and continues with the next tick.
Source code in src/watlowlib/devices/controller.py
read_parameter
async
¶
Read any registry parameter.
instance=1 is the default for single-loop devices and the
first loop / channel on multi-loop devices.
Source code in src/watlowlib/devices/controller.py
read_pv
async
¶
Read the process value for instance (loop number, 1-indexed).
Source code in src/watlowlib/devices/controller.py
read_setpoint
async
¶
Read the active setpoint for instance.
Source code in src/watlowlib/devices/controller.py
set_setpoint
async
¶
Write the setpoint and return the device-echoed value as a :class:Reading.
Setpoint is RWES — pass confirm=True to acknowledge the
EEPROM write. The returned reading is the device's echo of
the value it accepted.
Source code in src/watlowlib/devices/controller.py
write_parameter
async
¶
Write any registry parameter.
Persistent (RWE / RWES) writes require confirm=True;
the session raises :class:WatlowConfirmationRequiredError
before any I/O if the gate is missing.
Source code in src/watlowlib/devices/controller.py
Session¶
watlowlib.devices.session ¶
The :class:Session — single dispatch point for every command.
The session is the only place that gates, logs, and updates
:class:Availability. Variants are pure (ctx, request) → response
functions; protocol clients only own the wire. Per docs/design.md
invariant 2, no other layer touches these concerns.
Responsibilities (in order, per execute):
- Resolve the protocol variant.
UNSUPPORTEDis sticky — short- circuit pre-I/O on a typed error. - Enforce
confirm=Truefor :attr:SafetyTier.PERSISTENTwrites. - Acquire the per-port lock on the protocol client.
- Variant
encode→client.execute→ variantdecode. - Map success / typed errors to availability transitions and log a structured event.
Variant signatures differ across protocols (see
docs/design.md §5):
- Std Bus variants take
decode(reply, ctx)— the reply already carries the parameter selector echoed by the device. - Modbus variants take
decode(words, ctx, request)— the wire carries no echo, so the variant re-resolves the spec from the request to interpret the words.
Session ¶
Owns availability cache, gates, and the dispatch loop.
A :class:Session is bound to exactly one :class:ProtocolClient
for its lifetime — one protocol per port (invariant 1).
Source code in src/watlowlib/devices/session.py
client
property
¶
The bound protocol client.
Exposed for the watlow-raw escape hatch and for diagnostics
that need to issue an unframed wire op outside the registry.
Callers must acquire :attr:ProtocolClient.lock before
:meth:ProtocolClient.execute to honour the per-port
serialization invariant, and must pass this session's
:attr:address (or another concrete address for multi-drop
diagnostics) to execute.
registry
property
¶
Parameter registry bound to this session.
Exposed for the streaming layer so polling code can resolve a
name / id to a :class:ParameterSpec without an extra import
of the module-level :data:PARAMETERS.
availability ¶
dispose ¶
execute
async
¶
Dispatch command with request and return the typed response.
Source code in src/watlowlib/devices/session.py
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 | |
Loops¶
watlowlib.devices.loop ¶
Per-loop sub-facade returned by :meth:Controller.loop.
A :class:ControllerLoop is a thin view over a :class:Controller
that pre-binds an instance argument. It validates the loop number
once at construction (cross-cutting invariant 6: 1-indexed everywhere)
and forwards every operation to the parent controller's session,
threading the loop index as the registry instance.
The sub-facade is stateless beyond the loop number — it does not
duplicate the controller's transport, lock, or availability cache.
Multiple :class:ControllerLoop instances over the same controller
share the underlying session safely; concurrent calls serialize on
the protocol client's lock.
This module intentionally has no protocol-specific code. PID,
output, and alarm helpers live in :mod:watlowlib.commands.loop and
:mod:watlowlib.commands.alarms so the facade-only logic and the
parameter aggregation logic stay separate.
ControllerLoop ¶
A view over one control loop on a :class:Controller.
Construct via :meth:Controller.loop; never instantiated
directly by user code. The sub-facade lives only as long as the
parent controller's session — closing the controller is the only
cleanup needed.
Source code in src/watlowlib/devices/loop.py
read_alarms
async
¶
Read the alarm word for this loop.
Currently raises :class:watlowlib.errors.WatlowProtocolUnsupportedError —
see :func:watlowlib.commands.alarms.read_alarms for why the
decoder is not yet wired up.
Source code in src/watlowlib/devices/loop.py
read_output
async
¶
read_pid
async
¶
Read every PID gain for this loop. Missing gains return None.
Cool-side gains (cool_proportional_band, dead_band)
are skipped when the controller's identified capabilities
lack :attr:Capability.HAS_COOLING (e.g. PM output_2 ==
'A'). Pre-identify, the gate is permissive.
Source code in src/watlowlib/devices/loop.py
read_pv
async
¶
read_setpoint
async
¶
set_setpoint
async
¶
Write this loop's setpoint (RWES → confirm=True required).
Source code in src/watlowlib/devices/loop.py
write_pid
async
¶
Write the supplied gains for this loop.
Persistent — passing confirm=True is required. Fields
left None on gains skip the wire entirely. Setting a
cool-side field on a controller without
:attr:Capability.HAS_COOLING raises
:class:watlowlib.errors.WatlowConfigurationError.
Source code in src/watlowlib/devices/loop.py
Capability + safety + availability¶
watlowlib.devices.capability ¶
Three small enums that set the contract between layers.
- :class:
SafetyTier— derived from RWES; gatesconfirm=Truewrites. - :class:
Capability— coarse hardware-feature bitmap. Bits are added when a captured family needs them and existing values stay stable. - :class:
Availability— per-command session cache state.
This module is leaf — it imports nothing from
:mod:watlowlib.devices siblings, so the registry and command layers
can pull these enums without an import cycle. See docs/design.md
§5b.
Availability ¶
Bases: StrEnum
Per-command session state.
Sticky for the session: once a command transitions to
:attr:UNSUPPORTED, the session short-circuits subsequent
invocations with a typed error pre-I/O. The transition table
lives in docs/design.md §5b.
Capability ¶
Bases: Flag
Coarse hardware capability bits.
Bits are derived from a decoded part number when one is available
(see :func:watlowlib.registry.families.capabilities_for_part_number)
and fall back to a per-family prior otherwise. The session widens
the set at runtime when a command succeeds against a parameter
that proves the capability.
The vocabulary is small on purpose — most Watlow gating is by
:class:watlowlib.registry.families.ControllerFamily and by
:attr:watlowlib.registry.parameters.ParameterSpec.parameter_id,
not by per-feature bits. New bits are added when captured family
behaviour requires them.
SafetyTier ¶
Bases: IntEnum
How dangerous a command is to invoke.
READ_ONLY(R) — no state change.STATEFUL— runtime state change but not EEPROM-backed. Reserved for commands like "start autotune"; no PM parameter maps here today, but the tier exists so future commands have a place to live.PERSISTENT(RW / RWE / RWES) — EEPROM-backed; requiresconfirm=Trueat the facade.
capabilities_for_family ¶
Return the capability prior for family.
The session promotes observed capabilities at runtime and the part-
number decoder fills in per-SKU bits via
:func:watlowlib.registry.families.capabilities_for_part_number.
PM is intentionally :attr:Capability.NONE because PM SKUs vary
across every dimension (cooling / modbus / profile / comms).
Source code in src/watlowlib/devices/capability.py
Family classification¶
watlowlib.devices.kind ¶
Re-export :class:ControllerFamily under :mod:watlowlib.devices.
Callers can import it from either location. The canonical home is
:mod:watlowlib.registry.families so the registry layer can construct
family enums without depending on :mod:watlowlib.devices.
ControllerFamily ¶
Bases: StrEnum
Watlow controller family discriminator.
Membership here is advisory — :class:watlowlib.devices.session.Session
treats family hints as priors, not gates, unless the session was
opened with strict=True. See docs/design.md §5b.
classify_family ¶
Return the :class:ControllerFamily for a part-number string.
Only the leading family discriminator is parsed; per-family digit
decoding is in :func:decode_part_number.
Source code in src/watlowlib/registry/families.py
Public dataclasses¶
watlowlib.devices.models ¶
Public dataclasses returned by the :class:Controller facade.
All frozen, slots=True. py.typed ships.
See docs/design.md §6a.
AlarmState
dataclass
¶
Decoded alarm bits for one loop.
DeviceHealth ¶
Bases: StrEnum
Outcome of an :meth:Controller.identify call.
Used by callers (the maintenance verify pass, the configure CLI, discovery rows) to distinguish "the device answered every probe" from "the device answered some probes but not the load-bearing part-number read." Sentinel values stay the same enum across the public API so downstream code can branch on it.
DeviceInfo
dataclass
¶
DeviceInfo(
part_number,
hardware_id,
firmware_id,
serial_number,
family,
protocol,
address,
capabilities,
serial_settings,
loops,
health=DeviceHealth.OK,
configured_protocol=None,
)
Identity + connection metadata for an open controller.
Returned by :meth:Controller.identify. Capabilities are decoded
from the part number when one is captured (see
:func:watlowlib.registry.families.capabilities_for_part_number)
and OR-ed with the family prior; unobserved bits stay zero rather
than being guessed.
protocol is the wire protocol the host is currently talking;
configured_protocol is what the device's persistent EEPROM
parameter (PM 17009) reports. They normally match, but when they
diverge the helper :attr:protocol_mismatch flags it — useful
for catching SKU/firmware combinations where the user wrote a new
protocol but the runtime stack didn't pick it up (e.g. comms
position-8 = 'A', no Modbus stack present even though 17009 reads
1057).
protocol_mismatch
property
¶
True when EEPROM says one protocol and we're talking another.
Always False when :attr:configured_protocol is None
(i.e. identify did not query parameter 17009).
DiscoveryResult
dataclass
¶
One row from the discovery sweep.
LoopState
dataclass
¶
Snapshot of one loop. Composed from several reads.
ParameterEntry
dataclass
¶
Generic registry-driven read/write result.
Returned by :data:watlowlib.commands.READ_PARAMETER and
:data:watlowlib.commands.WRITE_PARAMETER. The
:class:Controller translates an entry into a :class:Reading /
:class:PartNumber / etc. when the public API guarantees a
richer shape.
PartNumber
dataclass
¶
Parsed part-number string returned by read_part_number.
Per-family digit decoding is contributed by
:mod:watlowlib.registry.families. Decoded fragments live in
:attr:details as a free-form mapping so each family can populate
only what its ordering format defines, and so adding fragments to
the PM decoder later is non-breaking.
The EZ-ZONE PM decoder populates case size, control type, power
input, three output codes, and options string. Other families fall
through to a stub: only :attr:family is set, and :attr:details
is empty.
Reading
dataclass
¶
A single timestamped value from the controller.
protocol is set by the variant decoder, not by the facade —
it reflects which wire protocol produced the value (per
docs/design.md invariant 7).
Factory¶
watlowlib.devices.factory ¶
open_device — single entry point for opening a controller.
Honours :attr:ProtocolKind.STDBUS, :attr:ProtocolKind.MODBUS_RTU,
and :attr:ProtocolKind.AUTO (Std Bus probe → Modbus probe → fail).
The detector itself lives in :mod:watlowlib.protocol.detect; the
factory only orchestrates.
The factory does not sweep bauds — the user sets one. See
docs/design.md §7 for why baud sweeping is opt-in via the
watlow-discover CLI rather than the open path.
open_controller
async
¶
open_controller(
transport,
*,
protocol,
address,
serial_settings,
family=ControllerFamily.UNKNOWN,
)
Build a :class:Controller over an existing :class:Transport.
Tests use this to drive the facade through a
:class:watlowlib.transport.fake.FakeTransport. Production code
uses :func:open_device.
Source code in src/watlowlib/devices/factory.py
open_device
async
¶
Open a controller on a serial port.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
port
|
str
|
Serial-port path ( |
required |
protocol
|
ProtocolKind
|
Wire protocol. |
STDBUS
|
address
|
int
|
Bus address. Std Bus accepts |
1
|
serial_settings
|
SerialSettings | None
|
Optional override. Default is 38400 8-N-1,
the EZ-ZONE PM Standard Bus factory setting; |
None
|
Returns:
| Type | Description |
|---|---|
Controller
|
An opened :class: |
Controller
|
detector held the transport open after a successful probe), |
Controller
|
otherwise an unopened :class: |
Controller
|
async context manager. |
Raises:
| Type | Description |
|---|---|
WatlowConfigurationError
|
|
WatlowProtocolUnsupportedError
|
|
Source code in src/watlowlib/devices/factory.py
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | |
Discovery¶
watlowlib.devices.discovery ¶
Address sweeps for watlow-discover.
Two sweep entry points, mirrored on the two protocols:
- :func:
sweep_stdbuswalks Standard Bus addresses1..16(BACnet MS/TP MAC0x10..0x1F). - :func:
sweep_modbuswalks a configurable Modbus slave-address range (defaults to1..16to match the common bench setup; callers can extend to1..247).
Each address probe issues one bounded read of parameter 1001
(Hardware ID — the auto-detect probe target) and returns a
:class:DiscoveryResult row. Successful probes promote into a full
:meth:Controller.identify call so the row carries a populated
:class:DeviceInfo.
The sweep opens the underlying transport once and reuses it across every address — Standard Bus addresses differ only in the dst-MAC byte of the BACnet MS/TP outer frame, and the Modbus bus driver multiplexes slaves over a single open serial handle. Reopening per address would add ~0.5s of cdc_acm re-init per probe on Linux; the open-once design lands a 16-address sweep in well under a second of wall-clock above the actual wire turnaround.
Discovery is opt-in — it is never run from open_device. The
watlow-discover CLI surfaces this module to the command line.
sweep_modbus
async
¶
sweep_modbus(
port,
*,
addresses=DEFAULT_MODBUS_RANGE,
serial_settings=None,
timeout_s=_DEFAULT_PROBE_TIMEOUT_S,
)
Yield one :class:DiscoveryResult per Modbus address probed.
Same shape as :func:sweep_stdbus — sequential, with the bus
transport opened once and reused across every slave address. The
Modbus driver multiplexes slaves over a single open handle so no
per-address transport churn is incurred.
Source code in src/watlowlib/devices/discovery.py
sweep_stdbus
async
¶
sweep_stdbus(
port,
*,
addresses=DEFAULT_STDBUS_RANGE,
serial_settings=None,
timeout_s=_DEFAULT_PROBE_TIMEOUT_S,
)
Yield one :class:DiscoveryResult per Std Bus address probed.
Opens the serial transport once, walks addresses sequentially
against the same open handle, then closes. Silent rows carry a
populated :attr:DiscoveryResult.error of type
:class:watlowlib.errors.WatlowTimeoutError (or
:class:watlowlib.errors.WatlowTransportError for framing issues)
so callers can distinguish "device absent" from "address never
tried".
timeout_s bounds every underlying parameter read; the default
keeps a 16-address silent sweep under five seconds.