pyisyox.schema.editor module

Editor dataclasses and bidirectional codec for IoX profile editors.

An editor (e.g. "I_OL", "I_TSTAT_MODE") is referenced by both nodedef properties and command parameters and defines a bidirectional contract — read-side decode and write-side validation/encode. Write-side constraints beyond min/max/prec are the subset mask ("0-3,5-7" excludes 4) and the names enum option list.

Send-side scaling

The controller does all device-side scaling itself, keyed off the UOM appended to the /cmd URL — proven on hardware: /cmd/DON/100/100 → 39 % (100 read as a UOM-100 0-255 byte) vs /cmd/DON/100/51 → 100 % (100 read as UOM-51 percent). So the codec validates input (enum-name resolution, min/max, subset) but sends the displayed value verbatim with its range UOM; it does not rewrite the number (no *10**prec rescale, no half-degree doubling). The eisy web UI does the same (/cmd/setTemp/10.4/17). decode keeps its precision-aware formatting for display helpers, but the property read path normalises by the wire UOM and never calls it.

class EditorRange(uom, min=None, max=None, precision=0, subset=<factory>, names=<factory>, step=None, nls_prefix=None)[source]

Bases: object

One range entry within an editor.

An editor may carry multiple ranges (e.g., a temperature editor with Fahrenheit and Celsius variants), each tied to a distinct UOM.

Variables:
  • uom (str) – Unit-of-measure identifier (string, indexes into the IoX UOM table).

  • min (float | None) – Lower numeric bound for raw values (inclusive). None when the range is purely enumerative (subset only).

  • max (float | None) – Upper numeric bound (inclusive).

  • precision (int) – Decimal precision applied to raw values (e.g., raw 6839 with precision=4 displays as 0.6839). The wire keys it as "prec"; Python attribute spells it out.

  • subset (set[int]) – Resolved set of valid raw integers, narrower than [min, max]. Empty when the full [min, max] range is valid.

  • names (dict[int, str]) – Mapping of raw integer → display name for enumerated values (e.g., {0: "Off", 1: "Heat", 2: "Cool"}).

  • step (float | None) – Increment hint for numeric (slider-shaped) ranges, in displayed units — e.g. 0.5 on a half-degree setpoint editor. None when the editor doesn’t specify one (callers then derive a step from precision). Not used by the codec; it’s a UI hint, surfaced for consumers that build number entities.

  • nls_prefix (str | None) – The NLS string-table prefix this range’s enum option names live under (the _N_<nls> segment of an encoded editor id, or a named editor’s index nls). names stays empty until something resolves it against an NLS table (the controller does it inline for /rest/profiles families; Profile.find_editor() does it for encoded editors when the profile has an NLS table loaded).

Parameters:
uom: str
min: float | None
max: float | None
precision: int
subset: set[int]
names: dict[int, str]
step: float | None
nls_prefix: str | None
classmethod from_json(raw)[source]

Build a range from a JSON object.

Parameters:

raw (dict)

Return type:

EditorRange

is_valid(raw_value)[source]

True if raw_value is acceptable for outbound commands.

Used for subset validation only (prec=0, enum-shaped editors). Numeric editors with prec>0 validate the displayed value and send it as-is (no scaling — the controller scales device-side from the UOM; see Editor.encode()). min/max in the IoX schema are stored in displayed form (e.g. min=5.0 on a UOM-4 °C setpoint editor with prec=1 means 5.0 °C, not raw 5).

Parameters:

raw_value (int)

Return type:

bool

exception EditorCodecError[source]

Bases: ValueError

Raised when an editor codec cannot encode or decode a value.

class Editor(id, ranges=<factory>)[source]

Bases: object

A profile editor — bidirectional codec for property and parameter values.

Encoding direction (encode): user input (int or enum name) → raw int suitable to send to the controller, with subset/range validation.

Decoding direction (decode): raw int from the controller → display string (enum name if known, else formatted number with prec/uom).

For multi-range editors the codec selects the range whose UOM matches a caller-supplied uom hint, falling back to the first range. Most editors carry a single range.

Parameters:
id: str
ranges: list[EditorRange]
classmethod from_json(raw)[source]

Build an Editor from a JSON object.

Parameters:

raw (dict)

Return type:

Editor

classmethod from_encoded_id(editor_id)[source]

Decode a self-describing encoded editor id into an Editor.

IoX lets a simple editor be referenced by an id that fully encodes its (single) range instead of pointing at a named <editor> element — handy for the dynamically-generated Z-Wave nodedefs where most editors are spelled inline. The grammar (see https://developer.isy.io/docs/API/IoX/editors#encoded-editor-id):

  • _<uom>_<prec> — implied bounds [-2147483647, 2147483647]

  • optionally one of _R_<min>_<max> (numeric range; a leading m makes a bound negative — _17_2_R_m5_10 => -5..10) or _S_<lowMask>[_<highMask>] (subset as a 32/64-bit hex bitmask — _17_1_S_FF00FF00{8..15, 24..31})

  • optionally a trailing _N_<nls> NLS-prefix segment

Returns None if editor_id doesn’t parse as an encoding (so callers can fall back to a table lookup). The _N_<nls> segment is captured as EditorRange.nls_prefix but not resolved here — Profile.find_editor() fills names from it when the profile carries an NLS table. Range / subset validation works regardless.

Parameters:

editor_id (str)

Return type:

Editor | None

range_for(uom=None)[source]

Pick the range matching uom, or the first range if no hint.

Parameters:

uom (str | None)

Return type:

EditorRange

decode(raw_value, uom=None)[source]

Decode a raw value to its display string.

Enum lookup first (when names covers the value), otherwise a precision-aware numeric string. Does not append the unit — callers format the unit separately based on the range’s uom.

When uom isn’t given and the editor has multiple ranges, the enum-name lookup scans every range (so e.g. an editor whose first range is a 0-100 % scale and whose second is a tiny {1: "Previous Value"} index still decodes 1 to its name).

UOM-101 / “degrees” with prec=0 halves the raw value (Insteon half-degree convention).

Parameters:
Return type:

str

encode(value, uom=None)[source]

Validate user input and return the value to put on the wire.

Two paths within a range:

  • Enum name (str matching ``names``) — returns the matching raw int verbatim. min/max don’t apply.

  • Numeric (int/float, or string parsed as float) — the displayed value. Validated against [min, max] and the subset mask (both stored in displayed form), then returned as-is (int when integral, else float). The controller does device-side scaling from the appended UOM — the codec does not rewrite the number (no *10**prec rescale, no half-degree doubling).

When uom is given, only that range is tried. Otherwise every range is tried in order and the first that accepts value wins — multi-range editors like ZW_DIM_PERCENT (range 0 is a tiny {1: "Previous Value"} index, range 1 is the 0-100 % scale) need this so a plain 75 lands in the percent range instead of being rejected by the index range.

Raises EditorCodecError if no range accepts value.

Parameters:
Return type:

int | float

encode_param(value, uom=None)[source]

Like encode(), but also returns the UOM of the range used.

Command-send code appends each parameter as /{value}/{uom} and the controller scales device-side from that UOM (proven: /cmd/DON/100/100 → 39 %, /cmd/DON/100/51 → 100 %), so the UOM has to be the one belonging to the range that actually accepted the value, not always ranges[0] — for a multi-range editor like ZW_DIM_PERCENT a plain 75 is encoded by the 0-100 % range (uom 51), so /cmd/DON/75/51 goes on the wire, not /cmd/DON/75/25.

Parameters:
Return type:

tuple[int | float, str]