Skip to content

watlowlib.registry

The cross-protocol parameter registry — PARAMETERS, ParameterRegistry, ParameterSpec, RwesFlag, family table, enumerations, and unit enums. See Parameters and Design §5a.

Public surface

watlowlib.registry

Parameter and family registry.

The registry is the cross-protocol seam: each parameter row carries both Std Bus selector (cls / member / instance) and Modbus selector (relative_addr / absolute_addr / register_count) metadata so command variants can lower a single read_parameter(...) call to either protocol with no per-parameter bespoke code. See docs/design.md §5a.

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.

OutputUnit

Bases: StrEnum

Display unit for an output parameter.

ParameterRegistry

ParameterRegistry(specs, *, aliases=DEFAULT_ALIASES)

Indexed view over a sequence of :class:ParameterSpec rows.

Lookups are O(1) on canonical name, alias, and parameter_id. Construction is O(N).

Source code in src/watlowlib/registry/parameters.py
def __init__(
    self,
    specs: tuple[ParameterSpec, ...],
    *,
    aliases: Mapping[str, str] = DEFAULT_ALIASES,
) -> None:
    # Apply name overrides + collect aliases per spec.
    rebound: list[ParameterSpec] = []
    for spec in specs:
        override = _NAME_OVERRIDES.get(spec.parameter_id)
        name = override or spec.name
        spec_aliases: set[str] = set()
        for alias, target in aliases.items():
            if target == name:
                spec_aliases.add(alias)
        if override and override != spec.name:
            # Keep the original auto-generated name as an alias so
            # callers that learn it from the JSON still resolve.
            spec_aliases.add(spec.name)
        if name != spec.name or spec_aliases:
            spec = ParameterSpec(  # noqa: PLW2901 — frozen dataclass rebind
                parameter_id=spec.parameter_id,
                name=name,
                aliases=frozenset(spec_aliases),
                data_type=spec.data_type,
                rwes=spec.rwes,
                safety=spec.safety,
                cls=spec.cls,
                member=spec.member,
                default_instance=spec.default_instance,
                max_instance=spec.max_instance,
                relative_addr=spec.relative_addr,
                absolute_addr=spec.absolute_addr,
                register_count=spec.register_count,
                word_order=spec.word_order,
                range_min=spec.range_min,
                range_max=spec.range_max,
                default=spec.default,
                family_hints=spec.family_hints,
            )
        rebound.append(spec)

    self._specs: tuple[ParameterSpec, ...] = tuple(rebound)
    by_id: dict[int, ParameterSpec] = {}
    by_name: dict[str, ParameterSpec] = {}
    for spec in self._specs:
        by_id[spec.parameter_id] = spec
        by_name[spec.name.lower()] = spec
        for alias in spec.aliases:
            by_name.setdefault(alias.lower(), spec)
    self._by_id: Mapping[int, ParameterSpec] = MappingProxyType(by_id)
    self._by_name: Mapping[str, ParameterSpec] = MappingProxyType(by_name)

has

has(name_or_id)

Return True if name_or_id resolves; never raises.

Source code in src/watlowlib/registry/parameters.py
def has(self, name_or_id: str | int) -> bool:
    """Return ``True`` if ``name_or_id`` resolves; never raises."""
    try:
        self.resolve(name_or_id)
    except WatlowValidationError:
        return False
    return True

resolve

resolve(name_or_id)

Look up a spec by canonical name, alias, or parameter ID.

Raises:

Type Description
WatlowValidationError

name_or_id does not resolve.

Source code in src/watlowlib/registry/parameters.py
def resolve(self, name_or_id: str | int) -> ParameterSpec:
    """Look up a spec by canonical name, alias, or parameter ID.

    Raises:
        WatlowValidationError: ``name_or_id`` does not resolve.
    """
    if isinstance(name_or_id, int):
        try:
            return self._by_id[name_or_id]
        except KeyError as exc:
            raise WatlowValidationError(
                f"unknown parameter id: {name_or_id}",
            ) from exc
    key = name_or_id.lower()
    try:
        return self._by_name[key]
    except KeyError as exc:
        raise WatlowValidationError(
            f"unknown parameter name: {name_or_id!r}",
        ) from exc

validate_instance

validate_instance(spec, instance)

Raise if instance is out of range for spec.

Public so the variant layer can validate before encoding.

Source code in src/watlowlib/registry/parameters.py
def validate_instance(self, spec: ParameterSpec, instance: int) -> None:
    """Raise if ``instance`` is out of range for ``spec``.

    Public so the variant layer can validate before encoding.
    """
    if instance < 1 or instance > spec.max_instance:
        raise WatlowValidationError(
            f"instance {instance} out of range for {spec.name!r} (1..{spec.max_instance})",
        )

validate_value

validate_value(spec, value)

Soft range check based on the spec's parsed range metadata.

Skipped silently if range_min / range_max couldn't be parsed from the JSON range field — Watlow's range strings are not always machine-readable. STRING parameters are not range-checked.

Source code in src/watlowlib/registry/parameters.py
def validate_value(self, spec: ParameterSpec, value: float | int | str) -> None:
    """Soft range check based on the spec's parsed ``range`` metadata.

    Skipped silently if ``range_min`` / ``range_max`` couldn't be
    parsed from the JSON ``range`` field — Watlow's range strings
    are not always machine-readable. STRING parameters are not
    range-checked.
    """
    if isinstance(value, str):
        return
    if spec.range_min is None or spec.range_max is None:
        return
    v = float(value)
    if v < spec.range_min or v > spec.range_max:
        raise WatlowValidationError(
            f"value {value!r} out of range for {spec.name!r} "
            f"({spec.range_min}..{spec.range_max})",
        )

ParameterSpec dataclass

ParameterSpec(
    parameter_id,
    name,
    aliases,
    data_type,
    rwes,
    safety,
    cls,
    member,
    default_instance,
    max_instance,
    relative_addr,
    absolute_addr,
    register_count,
    word_order=None,
    range_min=None,
    range_max=None,
    default=None,
    family_hints=_empty_family_hints(),
)

A single parameter row from pm_parameters.json.

Per-protocol fields:

  • Std Bus selector: :attr:cls, :attr:member, :attr:default_instance, :attr:max_instance.
  • Modbus selector: :attr:relative_addr, :attr:absolute_addr, :attr:register_count, :attr:word_order (None → client default HIGH_LOW per design §5a).

RwesFlag

Bases: StrEnum

Persistence + access flag from the EZ-ZONE register list.

  • R — read-only.
  • W — write-only (rare; typically actions like "start autotune").
  • RW — runtime read/write, not EEPROM-backed.
  • RWE — RW + persisted to EEPROM.
  • RWES — RWE + saved set ("save settings to user memory").

Mapping to :class:SafetyTier is in :func:_safety_from_rwes; the registry binds the result to :attr:ParameterSpec.safety at load time.

TemperatureUnit

Bases: StrEnum

Display unit for a temperature parameter.

classify_family

classify_family(part_number)

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
def classify_family(part_number: str) -> ControllerFamily:
    """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`.
    """
    head = part_number.strip().upper()
    if head.startswith("PM"):
        return ControllerFamily.PM
    if head.startswith("RM"):
        return ControllerFamily.RM
    if head.startswith("ST"):
        return ControllerFamily.ST
    if head.startswith("F4T"):
        return ControllerFamily.F4T
    return ControllerFamily.UNKNOWN

load_enumerations

load_enumerations()

Load and return all symbol rows from enumerations.json.

Section-header rows (where the value column is a string) are dropped; only (_, _, _, int) rows survive.

Source code in src/watlowlib/registry/enumerations.py
def load_enumerations() -> tuple[EnumerationRow, ...]:
    """Load and return all symbol rows from ``enumerations.json``.

    Section-header rows (where the value column is a string) are
    dropped; only ``(_, _, _, int)`` rows survive.
    """
    raw = files(_DATA_PACKAGE).joinpath(_FILENAME).read_text(encoding="utf-8")
    blob: list[dict[str, list[Any]]] = json.loads(raw)
    out: list[EnumerationRow] = []
    for entry in blob:
        row = entry.get("row")
        if row is None or len(row) != _ROW_COLUMN_COUNT:
            continue
        value = row[3]
        if not isinstance(value, int):
            # Header rows carry a string in the value column.
            continue
        out.append((row[0], str(row[1]), row[2], value))
    return tuple(out)

load_pm_parameters

load_pm_parameters()

Load and return every PM parameter spec from the bundled JSON.

Source code in src/watlowlib/registry/parameters.py
def load_pm_parameters() -> tuple[ParameterSpec, ...]:
    """Load and return every PM parameter spec from the bundled JSON."""
    raw_text = files(_DATA_PACKAGE).joinpath(_PM_FILENAME).read_text(encoding="utf-8")
    rows: list[dict[str, Any]] = json.loads(raw_text)
    out: list[ParameterSpec] = []
    seen_ids: set[int] = set()
    for raw in rows:
        spec = _build_spec(raw)
        if spec is None:
            continue
        if spec.parameter_id in seen_ids:
            # Duplicates across PM Map 1 / Map 2 sheets are real (same
            # parameter_id, different instance metadata) — keep only
            # the first occurrence; the registry exposes max_instance
            # so callers reach all loops.
            continue
        seen_ids.add(spec.parameter_id)
        out.append(spec)
    return tuple(out)

Parameter specs + registry

watlowlib.registry.parameters

Parameter registry — the cross-protocol seam.

Each row of data/pm_parameters.json is loaded once into a :class:ParameterSpec, indexed by canonical name (with aliases) and by parameter_id. The spec carries enough information to lower a read_parameter("setpoint") call to either Std Bus or Modbus with no per-parameter bespoke code.

Loading is eager: a module-level :data:PARAMETERS is built at import time so subsequent lookups are O(1) dict reads. Loading is also fail-loud for malformed rows — a row missing decode metadata for its declared :class:DataType (e.g. a PACKED row with no count) is not silently dropped; it is surfaced as an :class:watlowlib.errors.WatlowProtocolError at load time.

ParameterRegistry

ParameterRegistry(specs, *, aliases=DEFAULT_ALIASES)

Indexed view over a sequence of :class:ParameterSpec rows.

Lookups are O(1) on canonical name, alias, and parameter_id. Construction is O(N).

Source code in src/watlowlib/registry/parameters.py
def __init__(
    self,
    specs: tuple[ParameterSpec, ...],
    *,
    aliases: Mapping[str, str] = DEFAULT_ALIASES,
) -> None:
    # Apply name overrides + collect aliases per spec.
    rebound: list[ParameterSpec] = []
    for spec in specs:
        override = _NAME_OVERRIDES.get(spec.parameter_id)
        name = override or spec.name
        spec_aliases: set[str] = set()
        for alias, target in aliases.items():
            if target == name:
                spec_aliases.add(alias)
        if override and override != spec.name:
            # Keep the original auto-generated name as an alias so
            # callers that learn it from the JSON still resolve.
            spec_aliases.add(spec.name)
        if name != spec.name or spec_aliases:
            spec = ParameterSpec(  # noqa: PLW2901 — frozen dataclass rebind
                parameter_id=spec.parameter_id,
                name=name,
                aliases=frozenset(spec_aliases),
                data_type=spec.data_type,
                rwes=spec.rwes,
                safety=spec.safety,
                cls=spec.cls,
                member=spec.member,
                default_instance=spec.default_instance,
                max_instance=spec.max_instance,
                relative_addr=spec.relative_addr,
                absolute_addr=spec.absolute_addr,
                register_count=spec.register_count,
                word_order=spec.word_order,
                range_min=spec.range_min,
                range_max=spec.range_max,
                default=spec.default,
                family_hints=spec.family_hints,
            )
        rebound.append(spec)

    self._specs: tuple[ParameterSpec, ...] = tuple(rebound)
    by_id: dict[int, ParameterSpec] = {}
    by_name: dict[str, ParameterSpec] = {}
    for spec in self._specs:
        by_id[spec.parameter_id] = spec
        by_name[spec.name.lower()] = spec
        for alias in spec.aliases:
            by_name.setdefault(alias.lower(), spec)
    self._by_id: Mapping[int, ParameterSpec] = MappingProxyType(by_id)
    self._by_name: Mapping[str, ParameterSpec] = MappingProxyType(by_name)

has

has(name_or_id)

Return True if name_or_id resolves; never raises.

Source code in src/watlowlib/registry/parameters.py
def has(self, name_or_id: str | int) -> bool:
    """Return ``True`` if ``name_or_id`` resolves; never raises."""
    try:
        self.resolve(name_or_id)
    except WatlowValidationError:
        return False
    return True

resolve

resolve(name_or_id)

Look up a spec by canonical name, alias, or parameter ID.

Raises:

Type Description
WatlowValidationError

name_or_id does not resolve.

Source code in src/watlowlib/registry/parameters.py
def resolve(self, name_or_id: str | int) -> ParameterSpec:
    """Look up a spec by canonical name, alias, or parameter ID.

    Raises:
        WatlowValidationError: ``name_or_id`` does not resolve.
    """
    if isinstance(name_or_id, int):
        try:
            return self._by_id[name_or_id]
        except KeyError as exc:
            raise WatlowValidationError(
                f"unknown parameter id: {name_or_id}",
            ) from exc
    key = name_or_id.lower()
    try:
        return self._by_name[key]
    except KeyError as exc:
        raise WatlowValidationError(
            f"unknown parameter name: {name_or_id!r}",
        ) from exc

validate_instance

validate_instance(spec, instance)

Raise if instance is out of range for spec.

Public so the variant layer can validate before encoding.

Source code in src/watlowlib/registry/parameters.py
def validate_instance(self, spec: ParameterSpec, instance: int) -> None:
    """Raise if ``instance`` is out of range for ``spec``.

    Public so the variant layer can validate before encoding.
    """
    if instance < 1 or instance > spec.max_instance:
        raise WatlowValidationError(
            f"instance {instance} out of range for {spec.name!r} (1..{spec.max_instance})",
        )

validate_value

validate_value(spec, value)

Soft range check based on the spec's parsed range metadata.

Skipped silently if range_min / range_max couldn't be parsed from the JSON range field — Watlow's range strings are not always machine-readable. STRING parameters are not range-checked.

Source code in src/watlowlib/registry/parameters.py
def validate_value(self, spec: ParameterSpec, value: float | int | str) -> None:
    """Soft range check based on the spec's parsed ``range`` metadata.

    Skipped silently if ``range_min`` / ``range_max`` couldn't be
    parsed from the JSON ``range`` field — Watlow's range strings
    are not always machine-readable. STRING parameters are not
    range-checked.
    """
    if isinstance(value, str):
        return
    if spec.range_min is None or spec.range_max is None:
        return
    v = float(value)
    if v < spec.range_min or v > spec.range_max:
        raise WatlowValidationError(
            f"value {value!r} out of range for {spec.name!r} "
            f"({spec.range_min}..{spec.range_max})",
        )

ParameterSpec dataclass

ParameterSpec(
    parameter_id,
    name,
    aliases,
    data_type,
    rwes,
    safety,
    cls,
    member,
    default_instance,
    max_instance,
    relative_addr,
    absolute_addr,
    register_count,
    word_order=None,
    range_min=None,
    range_max=None,
    default=None,
    family_hints=_empty_family_hints(),
)

A single parameter row from pm_parameters.json.

Per-protocol fields:

  • Std Bus selector: :attr:cls, :attr:member, :attr:default_instance, :attr:max_instance.
  • Modbus selector: :attr:relative_addr, :attr:absolute_addr, :attr:register_count, :attr:word_order (None → client default HIGH_LOW per design §5a).

RwesFlag

Bases: StrEnum

Persistence + access flag from the EZ-ZONE register list.

  • R — read-only.
  • W — write-only (rare; typically actions like "start autotune").
  • RW — runtime read/write, not EEPROM-backed.
  • RWE — RW + persisted to EEPROM.
  • RWES — RWE + saved set ("save settings to user memory").

Mapping to :class:SafetyTier is in :func:_safety_from_rwes; the registry binds the result to :attr:ParameterSpec.safety at load time.

load_pm_parameters

load_pm_parameters()

Load and return every PM parameter spec from the bundled JSON.

Source code in src/watlowlib/registry/parameters.py
def load_pm_parameters() -> tuple[ParameterSpec, ...]:
    """Load and return every PM parameter spec from the bundled JSON."""
    raw_text = files(_DATA_PACKAGE).joinpath(_PM_FILENAME).read_text(encoding="utf-8")
    rows: list[dict[str, Any]] = json.loads(raw_text)
    out: list[ParameterSpec] = []
    seen_ids: set[int] = set()
    for raw in rows:
        spec = _build_spec(raw)
        if spec is None:
            continue
        if spec.parameter_id in seen_ids:
            # Duplicates across PM Map 1 / Map 2 sheets are real (same
            # parameter_id, different instance metadata) — keep only
            # the first occurrence; the registry exposes max_instance
            # so callers reach all loops.
            continue
        seen_ids.add(spec.parameter_id)
        out.append(spec)
    return tuple(out)

Family classification + part-number decoder

watlowlib.registry.families

Controller families and per-family part-number decoders.

The :class:ControllerFamily enum is the family discriminator used across the library. :func:classify_family parses the leading characters of a part number to that enum; :func:decode_part_number runs the family's full decoder when one is registered.

The EZ-ZONE PM decoder is the only full per-family decoder today. Other families fall through to the discriminator-only stub.

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.

capabilities_for_part_number

capabilities_for_part_number(part)

Decode :class:Capability bits from a parsed :class:PartNumber.

PM is the only family decoded today; other families return the family prior unchanged. Bits derived here are facts about the SKU — they do not depend on the device responding to any particular query, only on the part-number string.

Returns the family prior OR-ed with any decoded bits, so callers can use this as the authoritative seed for :attr:DeviceInfo.capabilities after :meth:Controller.identify captures the part number.

Source code in src/watlowlib/registry/families.py
def capabilities_for_part_number(part: PartNumber) -> Capability:
    """Decode :class:`Capability` bits from a parsed :class:`PartNumber`.

    PM is the only family decoded today; other families return the
    family prior unchanged. Bits derived here are *facts about the
    SKU* — they do not depend on the device responding to any
    particular query, only on the part-number string.

    Returns the family prior OR-ed with any decoded bits, so callers
    can use this as the authoritative seed for
    :attr:`DeviceInfo.capabilities` after :meth:`Controller.identify`
    captures the part number.
    """
    # Imported here to keep families.py a leaf module — the capability
    # enum lives under devices/ but families/ is depended on by
    # registry/parameters.py at import time.
    from watlowlib.devices.capability import (  # noqa: PLC0415
        Capability as _Capability,
    )
    from watlowlib.devices.capability import (  # noqa: PLC0415
        capabilities_for_family,
    )

    caps = capabilities_for_family(part.family)
    if part.family is not ControllerFamily.PM:
        return caps

    output_2 = part.details.get("output_2", "")
    if output_2 and output_2 != _PM_OUTPUT_NONE:
        caps |= _Capability.HAS_COOLING

    control = part.details.get("control_type", "")
    if control in _PM_CONTROL_PROFILES:
        caps |= _Capability.HAS_PROFILES | _Capability.PROFILE

    code = pm_comms_code(part)
    if code is not None:
        if code in _PM_COMMS_MODBUS:
            caps |= _Capability.HAS_MODBUS
        if code in _PM_COMMS_BLUETOOTH:
            caps |= _Capability.HAS_BLUETOOTH
        if code in _PM_COMMS_ETHERNET:
            caps |= _Capability.HAS_ETHERNET

    return caps

classify_family

classify_family(part_number)

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
def classify_family(part_number: str) -> ControllerFamily:
    """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`.
    """
    head = part_number.strip().upper()
    if head.startswith("PM"):
        return ControllerFamily.PM
    if head.startswith("RM"):
        return ControllerFamily.RM
    if head.startswith("ST"):
        return ControllerFamily.ST
    if head.startswith("F4T"):
        return ControllerFamily.F4T
    return ControllerFamily.UNKNOWN

decode_part_number

decode_part_number(raw)

Decode raw into a populated :class:PartNumber.

Dispatches to the per-family decoder based on :func:classify_family. Families without a decoder fall through to a bare :class:PartNumber carrying only the family discriminator.

Source code in src/watlowlib/registry/families.py
def decode_part_number(raw: str) -> PartNumber:
    """Decode ``raw`` into a populated :class:`PartNumber`.

    Dispatches to the per-family decoder based on
    :func:`classify_family`. Families without a decoder fall through
    to a bare :class:`PartNumber` carrying only the family
    discriminator.
    """
    # Imported here so this module stays a dependency-light leaf — the
    # ``models`` module pulls a handful of registry types and we don't
    # want a circular import on the family enum.
    from watlowlib.devices.models import PartNumber  # noqa: PLC0415

    family = classify_family(raw)
    if family is ControllerFamily.PM:
        _, details = _decode_pm(raw)
        return PartNumber(raw=raw, family=family, details=MappingProxyType(details))
    return PartNumber(raw=raw, family=family, details=MappingProxyType({}))

default_loops

default_loops(part)

Return the default loop count for the controller behind part.

Used by :class:Controller.identify to seed :attr:DeviceInfo.loops and by :meth:Controller.loop to validate the n argument. Returns 1 whenever the family or digits are unknown — never raises.

Source code in src/watlowlib/registry/families.py
def default_loops(part: PartNumber) -> int:
    """Return the default loop count for the controller behind ``part``.

    Used by :class:`Controller.identify` to seed
    :attr:`DeviceInfo.loops` and by :meth:`Controller.loop` to
    validate the ``n`` argument. Returns ``1`` whenever the family or
    digits are unknown — never raises.
    """
    if part.family is ControllerFamily.PM:
        case = part.details.get("case_size", "")
        ctrl = part.details.get("control_type", "")
        return _PM_LOOPS.get((case, ctrl), 1)
    # RM is multi-loop in production but no decoder is wired up yet;
    # default to 1 so callers don't get surprise behaviour.
    return 1

pm_comms_code

pm_comms_code(part)

Return the position-8 comms character of a PM part number, or None.

Position 8 is the first character of the 7-char options block — e.g. PM3R1CA-AAAAAAA has comms code A (Standard Bus only). Returns None for non-PM families and for PM part numbers without a parsed options string.

Source code in src/watlowlib/registry/families.py
def pm_comms_code(part: PartNumber) -> str | None:
    """Return the position-8 comms character of a PM part number, or ``None``.

    Position 8 is the first character of the 7-char options block —
    e.g. ``PM3R1CA-AAAAAAA`` has comms code ``A`` (Standard Bus only).
    Returns ``None`` for non-PM families and for PM part numbers
    without a parsed options string.
    """
    if part.family is not ControllerFamily.PM:
        return None
    options = part.details.get("options", "")
    if not options:
        return None
    return options[0]

pm_comms_supports_modbus

pm_comms_supports_modbus(part)

Whether the part's comms position-8 character carries Modbus.

Source code in src/watlowlib/registry/families.py
def pm_comms_supports_modbus(part: PartNumber) -> bool:
    """Whether the part's comms position-8 character carries Modbus."""
    code = pm_comms_code(part)
    return code is not None and code in _PM_COMMS_MODBUS

Enumerations

watlowlib.registry.enumerations

Loader for data/enumerations.json.

The enumerations file groups the symbolic names Watlow uses for parameter values (heat algorithms, sensor types, alarm states, ...). It is shaped as a flat list of rows, where each row is one of:

  • a 4-tuple [7Seg, PC label, text enumeration, value] — the actual symbol entry
  • a 4-tuple where the last element is a string column header (e.g. [..., "Value"]) — section header rows; skipped on load.

This module only loads the table. Binding specific symbol groups to :class:watlowlib.registry.parameters.ParameterSpec.enum happens elsewhere, once families and per-parameter enum metadata are wired through.

load_enumerations

load_enumerations()

Load and return all symbol rows from enumerations.json.

Section-header rows (where the value column is a string) are dropped; only (_, _, _, int) rows survive.

Source code in src/watlowlib/registry/enumerations.py
def load_enumerations() -> tuple[EnumerationRow, ...]:
    """Load and return all symbol rows from ``enumerations.json``.

    Section-header rows (where the value column is a string) are
    dropped; only ``(_, _, _, int)`` rows survive.
    """
    raw = files(_DATA_PACKAGE).joinpath(_FILENAME).read_text(encoding="utf-8")
    blob: list[dict[str, list[Any]]] = json.loads(raw)
    out: list[EnumerationRow] = []
    for entry in blob:
        row = entry.get("row")
        if row is None or len(row) != _ROW_COLUMN_COUNT:
            continue
        value = row[3]
        if not isinstance(value, int):
            # Header rows carry a string in the value column.
            continue
        out.append((row[0], str(row[1]), row[2], value))
    return tuple(out)

Aliases

watlowlib.registry.aliases

Friendly aliases for canonical parameter names.

Aliases like pvprocess_value and spsetpoint back the public read_parameter("pv") / read_parameter("setpoint") entry points. The :class:watlowlib.registry.parameters.ParameterRegistry consults this table when resolving a string that doesn't match a canonical name directly. Aliases are case-insensitive.

Adding new aliases is non-breaking — registry resolution is lookup, not generative.

Units

watlowlib.registry.units

Compact unit vocabulary for Watlow parameters.

Watlow doesn't have anything like Alicat's gas zoo. Most parameters are unitless or temperature; output values are percent. The enums are small intentionally — each one comes from an observed field on a real PM3 capture or a parameter whose unit is forced by Watlow firmware (e.g. percent for output).

Bound to :class:watlowlib.devices.models.Reading.unit by the command variants.

OutputUnit

Bases: StrEnum

Display unit for an output parameter.

TemperatureUnit

Bases: StrEnum

Display unit for a temperature parameter.