Skip to content

Latest commit

 

History

History
120 lines (83 loc) · 10.5 KB

File metadata and controls

120 lines (83 loc) · 10.5 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Overview

PyISY is the original async Python library for Universal Devices ISY-994 / IoX controllers (Polisy, eisy). It is the upstream package on PyPI (pyisy) used by the Home Assistant Core isy994 integration. This is the v3.x.x branch (asyncio + aiohttp). The threaded v2.x.x branch is no longer the default.

This package has a successor, PyISYoX, which is a from-scratch rewrite by the v3 contributor with a different API (entity model, ISYConnectionInfo dataclass, helpers/ submodule, etc.). The HACS beta integration hacs-isy994 consumes PyISYoX. Do not import PyISYoX patterns into PyISY — keep the public API stable since Home Assistant Core depends on it.

Commands

# Install dev + test dependencies + editable install
pip install -r requirements.txt -r requirements-dev.txt -r requirements-test.txt
pip install -e .

# Set up pre-commit (CI runs the same)
pre-commit install
pre-commit run --all-files

# Run the offline pytest suite
pytest                                      # full run
pytest --cov=pyisy --cov-report=term-missing
pytest tests/test_nodes.py                  # one file
pytest --snapshot-update                    # refresh syrupy snapshots after intentional behavior changes

# Smoke-test against a real ISY (also a usable script template)
python3 -m pyisy http://your-isy-url:80 username password
python3 -m pyisy -h         # full options
python3 -m pyisy -v ...     # verbose logging
python3 -m pyisy -q ...     # no event stream
python3 -m pyisy -n ...     # also load node servers

# Build docs
cd docs && make html

CI (.github/workflows/ci.yml) runs two jobs: pre-commit (lint) and tests (pytest on Python 3.11 and 3.14, the HA-supported range). Coverage prints to the workflow log; there's no external coverage service wired up. The suite is fully offline — fixtures under tests/fixtures/ are anonymized real-controller XML and tests/conftest.FakeConnection serves them, so no live ISY is required. Snapshots live under tests/__snapshots__/*.ambr and are tracked. For behaviors that genuinely need a controller (event stream / websocket reconnect, REST round-trips), still validate via python3 -m pyisy ....

Lint stack (all run by pre-commit):

  • ruff (lint + format) — config in pyproject.toml, target py39, line length 110
  • pyupgrade (--py39-plus)
  • codespell with an ISY-specific ignore list (isy, nid, dof, don, BATLVL, etc. — extend it if codespell flags a real ISY term)
  • yamllint, prettier

Architecture

Entry point and lifecycle

pyisy/isy.py — the ISY class is the single object users construct. Construction does no I/O. await isy.initialize() is required and performs:

  1. conn.test_connection() → parses /rest/config into a Configuration.
  2. If the model is not ISY 994, calls conn.increase_available_connections() — IoX hardware allows far more concurrent connections than ISY-994 (the connection semaphore is sized accordingly in connection.py).
  3. Fires off asyncio.gather() for status, time, nodes, programs, variable defs, variables, and (conditionally) network resources, then constructs Clock, Nodes, Programs, Variables, NetworkResources from the resulting XML.
  4. await self.nodes.update(xml=...) applies status to nodes.

await isy.shutdown() stops the websocket / event stream and closes the aiohttp session.

Connection layer (connection.py)

Connection owns the aiohttp session and all REST I/O. Notable details:

  • Uses an asyncio Semaphore to enforce ISY-994's strict simultaneous-connection limit (2 HTTPS / 5 HTTP); increase_available_connections() raises it for IoX.
  • request() handles retries with backoff, swallows expected 404s when ok404=True, and force-decodes responses as UTF-8 with errors="ignore" (ISY firmware emits invalid UTF-8 in some node names — see CHANGELOG #126).
  • request(retry404=True) makes the 404 branch fall into the retry/backoff loop instead of returning None. Use this from command-issuing callers (NodeBase.send_cmd, Folder.send_cmd, Variable.set_value, ISY.query, ISY.send_x10_cmd) — the ISY-994 firmware emits spurious 404s on /rest/nodes/.../cmd/... when its Insteon network is overwhelmed (#184). Don't enable on read paths; the existing retry budget is bounded (5 retries, ~3.4s) but multiplying that across status polls is wasteful.
  • get_new_client_session(use_https, tls_ver) and get_sslcontext() are module-level helpers because the ISY-994 supports legacy TLS 1.1 and weak ciphers; consumers (HA Core) need to obtain a session compatible with the device.

Event streams (events/)

Two mutually-exclusive transports for real-time updates:

  • websocket.WebSocketClient — preferred, opt in via ISY(use_websocket=True). Started/stopped explicitly with isy.websocket.start() / .stop().
  • tcpsocket.EventStream — legacy SOAP-over-TCP socket subscription. Lazily constructed when isy.auto_update = True is set. Reconnect is handled by a daemon Thread (_auto_reconnecter in isy.py) that recreates the EventStream on disconnect.

Setting auto_update while websockets are enabled is a no-op with a warning — the two paths must not be mixed. Connection-state notifications go through isy.connection_events (an EventEmitter) using the ES_* constants (ES_CONNECTED, ES_RECONNECTING, ES_RECONNECT_FAILED, etc.).

eventreader.ISYEventReader parses raw event XML; the ISY class itself dispatches system_status_changed_received() and node manager _node_changed_received() from parsed events.

Platform managers

Each platform module is a dict-like collection that owns its entities and exposes an EventEmitter for changes:

  • nodes/Nodes manages both Node (devices) and Group (scenes); nodebase.NodeBase is the shared base. NodeChangedEvent (in nodes/__init__.py) is the payload published on isy.nodes.status_events. Group/Scene status excludes Insteon battery devices and stateless controllers (ControlLinc / RemoteLinc v1) — see CHANGELOG #156, #256.
  • programs/Programs indexes both Program and programs.folder.Folder. Folders expose a .leaf property used to invoke folder-level run actions.
  • variables/Variables is a 2-level mapping: isy.variables[type][id] where type 1 = integer, 2 = state.
  • networking.NetworkResources / NetworkCommand — only loaded when Configuration[CONFIG_NETWORKING] or CONFIG_PORTAL is set.
  • node_servers.NodeServers — Polyglot node server definitions; loaded only if with_node_servers=True is passed to initialize().
  • clock.Clock — derived from /rest/time; provides sunrise/sunset/tz info.
  • configuration.Configuration — dict-like wrapper around the parsed /rest/config XML; consult it for feature flags before triggering optional REST calls.

Helpers, constants, logging

  • helpers.py contains EventEmitter / EventListener (the pub/sub primitives every manager uses) plus the XML-walking utilities (value_from_xml, attr_from_xml, attr_from_element, …). Most parsing in this codebase still uses xml.dom.minidom directly.
  • constants.py is large (commands, URLs, UOM tables, node families, system-status codes, ES_* connection events, NODE_CHANGED_ACTIONS, X10_COMMANDS). Add new constants here rather than scattering literals.
  • logging.py_LOGGER is the package-wide logger under the pyisy namespace; events use a separate pyisy.events logger so consumers can filter. Call enable_logging() once at startup.

Conventions when editing

  • Public API stability: pyisy.ISY, pyisy.connection.Connection, the manager classes, and the ISY*Error exceptions in pyisy/__init__.py are consumed by Home Assistant Core. Renaming or changing signatures is a breaking change. If you find yourself wanting a different shape, that work belongs in PyISYoX, not here.
  • No presumptive status updates: since v3.0.0, sending a command does not optimistically update node.status. Consumers that don't run an event stream are expected to call node.update(wait_time=UPDATE_INTERVAL) themselves.
  • UOM/precision sentinel: ISY_PROP_NOT_SET = "-1" distinguishes "node has no ST property" from ISY_VALUE_UNKNOWN ("status not yet reported"). Don't conflate them — see CHANGELOG v2.1.0 #98.
  • XML decoding: always go through Connection.request() so the UTF-8-with-ignore decode is applied. Don't read aiohttp responses directly.
  • Ruff ignores in pyproject.toml are deliberate (e.g. S101, SLF001, PLR091*); don't try to fix what's listed there as part of unrelated work.
  • ISYResponseParseError is the canonical "controller returned bad/missing data" signal. Every parse() method raises it on XML_ERRORS, and ISY.initialize() raises it when load-bearing setup responses (status, time, nodes, programs) are None so HA Core can convert that into ConfigEntryNotReady and retry instead of silently mounting an empty controller (see #297).
  • Test fixtures must stay anonymized. Anything added or refreshed under tests/fixtures/ must have Insteon prefixes randomized (the 3-byte device prefix; keep the 4th byte so subnode/group references stay consistent across nodes.xml / status.xml / programs.xml), personal names replaced, and lat/long zeroed. Z-Wave addresses (ZY007_1) and folder/group integer ids are not Insteon device addresses — leave them. A reproducible scrubber lives outside the repo at ~/src/.devcontainer_shared/home-assistant-core/isy994_stashed/anonymize_insteon_addresses.py; re-run with the same seed if you need new captures.
  • Test design. Tests use tests.conftest.FakeConnection (serves canned XML from tests/fixtures/) plus a build_isy(nodes_xml, status_xml) factory for feature-specific scenarios. Action-method tests (climate, lock, program/folder, node commands) replace isy.conn.request with an AsyncMock and assert the URL it was invoked with — the connection's real compile_url runs so URL-encoding regressions surface. Snapshots (syrupy) capture only structural/feature-flag summaries, never raw fixture contents.

Branches

  • v3.x.x — current default branch; async, what every consumer ships.
  • v2.x.x — legacy synchronous/threaded line; only fix here if explicitly asked.

External resources