pyisyox.schema package

Schema dataclasses for IoX 6 /rest/profiles JSON.

This package vendors the schema layer of UDI’s nucore-ai library: the type-equivalents for cmd, editor, linkdef, nodedef, and uom, plus a JSON-input loader for the unified profile blob.

The schema is intentionally separated from the wire layer so that connection code in pyisyox.connection can be rewritten freely while the data model remains stable. Editors carry a bidirectional codec (see pyisyox.schema.editor) covering both controller→display decoding and command-parameter encoding with subset/range validation.

class Command(id, name='', parameters=<factory>, native=False, format=None)[source]

Bases: object

A command a node sends or accepts.

Variables:
  • id (str) – Command identifier (e.g., "DON", "CLISPC", "DISCOVER").

  • name (str) – Human-readable label.

  • parameters (list[pyisyox.schema.cmd.CommandParameter]) – Positional parameters; empty for parameterless commands.

  • native (bool) – Whether the command is a native IoX command ("true") or implemented at a higher layer.

  • format (str | None) – Optional display format string used by the controller’s UI.

Parameters:
id: str
name: str
parameters: list[CommandParameter]
native: bool
format: str | None
classmethod from_json(raw)[source]

Build a Command from a JSON object as found in /rest/profiles nodedef cmds.sends[] / cmds.accepts[].

Defensive against partial / null fields under PG3 dynamic profile reload — a parameter without an editor key is skipped rather than raising KeyError on the whole nodedef.

Parameters:

raw (dict)

Return type:

Command

class CommandParameter(editor_id, param_id='', init=None, optional=False)[source]

Bases: object

A single positional parameter on a command.

Variables:
  • editor_id (str) – Reference to the editor defining valid values for this parameter. Resolves against the parent profile’s editor table.

  • param_id (str) – Optional parameter identifier (often empty in IoX).

  • init (str | None) – Optional property whose current value seeds this parameter (e.g., "CLISPH" — the heat setpoint command’s parameter initialises from the current CLISPH property).

  • optional (bool) – Whether the parameter may be omitted on send.

Parameters:
  • editor_id (str)

  • param_id (str)

  • init (str | None)

  • optional (bool)

editor_id: str
param_id: str
init: str | None
optional: bool
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]

exception EditorCodecError[source]

Bases: ValueError

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

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

class Family(id, name, instances=<factory>)[source]

Bases: object

A family of instances. Family id is a string so plugin slots ("10", "11", …) and the special "common" family can coexist.

Parameters:
id: str
name: str
instances: dict[str, Instance]
class Instance(id, name, editors=<factory>, linkdefs=<factory>, nodedefs=<factory>)[source]

Bases: object

One instance within a family — a self-contained set of editors, linkdefs, and nodedefs.

For built-in families there is typically one instance with id "1". For PG3 plugin families (family "10" in the captured fixture), the instance id matches the plugin slot number and is encoded in node addresses as the n0XX_ prefix.

Parameters:
id: str
name: str
editors: dict[str, Editor]
linkdefs: dict[str, LinkDef]
nodedefs: dict[str, NodeDef]
class LinkDef(id, parameters=<factory>)[source]

Bases: object

A link definition, identified by id and carrying parameter slots.

Variables:
Parameters:
id: str
parameters: list[LinkParameter]
classmethod from_json(raw)[source]

Build a LinkDef from a JSON object.

Defensive against partial / null fields under PG3 dynamic profile reload.

Parameters:

raw (dict)

Return type:

LinkDef

class LinkParameter(editor_id, param_id='', init=None)[source]

Bases: object

A single parameter on a link definition.

Variables:
  • editor_id (str) – Reference to the editor defining valid values.

  • param_id (str) – Parameter identifier within the link record.

  • init (str | None) – Optional initial-value source.

Parameters:
  • editor_id (str)

  • param_id (str)

  • init (str | None)

editor_id: str
param_id: str
init: str | None
class NLSTable(entries=<factory>)[source]

Bases: object

A parsed NLS string table — a flat key -> value map plus the handful of IoX key-shape lookups callers actually need.

Parameters:

entries (dict[str, str])

entries: dict[str, str]
classmethod parse(text)[source]

Parse KEY = VALUE text. Blank lines and # comments are skipped; the value keeps everything after the first = (so format strings containing = survive).

Parameters:

text (str)

Return type:

NLSTable

overlay(other)[source]

Return a new table with other’s entries layered on top of this one’s (other wins on key collisions).

Parameters:

other (NLSTable)

Return type:

NLSTable

command_name(command_id, base=None)[source]

Label for a command id, preferring the nodedef-scoped override.

Parameters:
  • command_id (str)

  • base (str | None)

Return type:

str | None

property_name(property_id, base=None)[source]

Label for a property id, preferring the nodedef-scoped override.

Parameters:
  • property_id (str)

  • base (str | None)

Return type:

str | None

nodedef_name(base)[source]

Default display name for a nodedef by its nls base.

Parameters:

base (str)

Return type:

str | None

enum_names(prefix)[source]

All <prefix>-<int> = label entries as an {int: label} map.

Used to resolve the option labels of an editor that references an NLS prefix (e.g. the encoded _..._N_IX_DIM_REP editor’s 0 -> "Off" / 101 -> "Unknown"). Non-integer suffixes (-NAME, -FMT, …) are ignored.

Parameters:

prefix (str)

Return type:

dict[int, str]

class NodeCommands(sends=<factory>, accepts=<factory>)[source]

Bases: object

Commands a nodedef sends and accepts.

Variables:
  • sends (list[pyisyox.schema.cmd.Command]) – Commands the node emits — useful as trigger sources (e.g. OnOffControl sends DON/DOF on physical press).

  • accepts (list[pyisyox.schema.cmd.Command]) – Commands the node receives — drive the node’s controllable HA platform (light/switch/climate/lock/cover/button).

Parameters:
sends: list[Command]
accepts: list[Command]
class NodeDef(id, family_id, instance_id, name='', properties=<factory>, cmds=<factory>, nls_key=None, links=<factory>)[source]

Bases: object

The static definition of a node class.

Variables:
  • id (str) – Nodedef identifier (e.g. "KeypadDimmer_ADV", "Thermostat", "flume2", "controller").

  • family_id (str) – Family id this nodedef belongs to ("1" for Insteon, "4" for Z-Wave, plugin slot id for PG3 nodedefs).

  • instance_id (str) – Instance id within the family (typically equal to family_id for built-in families and equal to the plugin slot for PG3 instances).

  • name (str) – Default display name (the NDN-<nls>-NAME NLS entry). Often empty — the live node carries a user-assigned name; this is just the discovery-time default. /rest/profiles families resolve it inline; for dynamic Z-Wave nodedefs pyisyox fills it from the family NLS table.

  • properties (dict[str, pyisyox.schema.nodedef.NodeProperty]) – Property slots, keyed by property id.

  • cmds (pyisyox.schema.nodedef.NodeCommands) – Sent and accepted commands.

  • nls_key (str | None) – Reference key into the NLS string table (e.g. "flume2"); pyisyox does not need to resolve this — every visible string is already inline-resolved in property/command name fields and in WS event frames.

  • links (pyisyox.schema.nodedef.NodeLinks) – Control and response link references.

Parameters:
id: str
family_id: str
instance_id: str
name: str
properties: dict[str, NodeProperty]
cmds: NodeCommands
nls_key: str | None
classmethod from_json(raw, family_id, instance_id)[source]

Build a NodeDef from a JSON object scoped to its family/instance.

Parameters:
Return type:

NodeDef

property lookup_key: tuple[str, str, str]

The (nodedef_id, family_id, instance_id) join key used to match a node from /api/nodes to its definition.

Bases: object

Control and response link references on a nodedef.

Parameters:
ctl: list[str]
rsp: list[str]
class NodeProperty(id, editor_id, name='', hide=False)[source]

Bases: object

A property slot defined on a nodedef.

Variables:
  • id (str) – Property id (e.g. "ST", "OL", "CLISPC", "GV1").

  • editor_id (str) – Reference to the editor governing this property’s display and (where applicable) write-side validation.

  • name (str) – Human-readable label, inline-resolved by the controller (e.g. "Current" for Flume’s GV1, "On Level" for Insteon’s OL). Authoritative source.

  • hide (bool) – Hint that the property should not be surfaced in default UIs.

Parameters:
id: str
editor_id: str
name: str
hide: bool
class Profile(timestamp='', families=<factory>, nodedef_lookup=<factory>, nls=<factory>)[source]

Bases: object

The decoded result of one /rest/profiles JSON blob.

The nodedef_lookup is the load-bearing artifact callers use to resolve a node (which carries family_id, instance_id, and nodeDefId) to its NodeDef.

Parameters:
timestamp: str
families: dict[str, Family]
nodedef_lookup: dict[tuple[str, str, str], NodeDef]
nls: NLSTable

Merged NLS string table for any dynamically-loaded families (Z-Wave). Empty unless pyisyox.client fetched per-family NLS during load.

classmethod load_from_json(raw)[source]

Build a Profile from a parsed /rest/profiles response.

Parameters:

raw (dict) – Top-level dict with timestamp and families[].

Returns:

A populated Profile with families, instances, and a built lookup table.

Return type:

Profile

merge(other)[source]

Merge other into self in place; return a diff summary.

Designed for PG3 dynamic profile reload. Existing runtime Node instances hold references to the resolved NodeDef, so a wholesale replace would leave them clinging to stale lookups — instead the merge mutates the existing dicts and replaces individual NodeDef/Editor/LinkDef objects. Additive only: items absent from other are kept.

Parameters:

other (Profile)

Return type:

ProfileMergeResult

register_nodedefs(family_id, instance_id, nodedefs)[source]

Add a batch of nodedefs to family_id/instance_id in place.

Used for dynamic Z-Wave nodedefs (fetched from def/get XML — the Z-Wave families already exist in /rest/profiles with their ZW_* editors but no nodedefs). Overwrites by id.

Parameters:
Return type:

None

find_nodedef(nodedef_id, family_id, instance_id)[source]

Resolve a nodedef by its (id, family, instance) join key.

Returns None when no matching nodedef exists — callers should treat that as the unknown-type case (e.g. fall back to the nodedef-driven HA platform classifier rather than the type-based one).

Parameters:
  • nodedef_id (str)

  • family_id (str)

  • instance_id (str)

Return type:

NodeDef | None

find_editor(editor_id, family_id, instance_id)[source]

Resolve an editor by id within a family/instance scope.

Editors are scoped to their instance — the same id (e.g. "bool") may appear in multiple instances with different ranges, so the family/instance must be supplied.

An encoded editor id — one that fully describes its range, e.g. "_51_0_R_0_101_N_IX_DIM_REP" — is decoded directly via Editor.from_encoded_id(); this is how the dynamically- generated Z-Wave nodedefs spell most of their editors. (The check is “does it parse as an encoding”, not just “starts with _” — UDI also ships named editors that begin with _ such as _sys_notify_full, which fall through to the table lookup.)

Table-lookup fallback chain on miss:

  1. family_id / instance_id (the requested scope)

  2. The "common" family / instance "1" — UDI publishes a shared set of editors there (_sys_notify_full, etc.) that any plugin nodedef can reference

Returns None if it’s neither a valid encoding nor present in either scope.

Parameters:
  • editor_id (str)

  • family_id (str)

  • instance_id (str)

Return type:

Editor | None

to_dict()[source]

Flatten the profile to a JSON-compatible dict.

nodedef_lookup is dropped — its (nodedef_id, family_id, instance_id) tuple keys are not JSON-encodable and the same data lives under families[fam].instances[inst].nodedefs. nodedef_lookup_count is surfaced as a sanity-check counter.

pyisyox.schema.editor.EditorRange carries subset: set[int] which JSON can’t encode either; the walker below normalises every set into a sorted list so the snapshot round-trips through json.dumps.

Return type:

dict[str, Any]

class Property(id, value, formatted='', uom='', prec=None, name='')[source]

Bases: object

A live property value reported by the controller for a node.

Variables:
  • id (str) – Property id (e.g. "ST", "GV1").

  • value (str) – Raw value as reported by the controller (string form keeps controller-emitted precision).

  • formatted (str) – Human-readable value (e.g. "0.6839 US gallons").

  • uom (str) – Unit-of-measure id reported alongside the value.

  • prec (int | None) – Decimal precision applied to value (None when not provided).

  • name (str) – Optional display name override (often empty — the nodedef-level NodeProperty.name is the authoritative label).

Parameters:
id: str
value: str
formatted: str
uom: str
prec: int | None
name: str
class UOMEntry(id, name, description='', category_id=None)[source]

Bases: object

A unit-of-measure entry.

Variables:
  • id (str) – UOM identifier as it appears in profile responses.

  • name (str) – Short display name (suitable for sensor unit_of_measurement).

  • description (str) – Free-text description.

  • category_id (str | None) – Coarse semantic category (see module docstring).

Parameters:
  • id (str)

  • name (str)

  • description (str)

  • category_id (str | None)

id: str
name: str
description: str
category_id: str | None
get_uom(uom_id)[source]

Look up a UOM by id; returns the unknown entry if unrecognised.

Parameters:

uom_id (str)

Return type:

UOMEntry

Submodules