Skip to content

Releases: automicus/PyISY

v3.6.1 - TLS modernization + offline test suite

Choose a tag to compare

@shbatm shbatm released this 08 May 16:25
24b1523

Heads-up: the default TLS configuration changes from a pinned 1.1 to "auto". eisy/Polisy IoX firmware now rejects TLS ≤1.1 per RFC 8996, so the new default is required to talk to current controllers. ISY-994 firmware 4.5.4+ defaults to TLS 1.2 and works with "auto" unmodified.

⚠️ Behavior changes

  • TLS default flipped to "auto"; numeric tls_ver values deprecated. get_sslcontext switches to ssl.PROTOCOL_TLS_CLIENT and the "auto" default sets minimum_version=TLSv1_2 with no max pin, so OpenSSL negotiates the highest version both peers support — TLS 1.3 against eisy/Polisy IoX, TLS 1.2 against stock ISY-994. Numeric tls_ver (1.1/1.2/1.3) still works behind a DeprecationWarning; pin only when an ISY-994 has been manually downgraded below TLS 1.2. Removes PROTOCOL_TLSv1_X deprecation noise on Python 3.12+. (#494, #499)
  • New verify_ssl: bool = False kwarg on ISY, Connection, WebSocketClient, and get_sslcontext. False (default) preserves CERT_NONE for self-signed controllers; True opts into CERT_REQUIRED + check_hostname for users with a properly signed certificate. (#494, #499)
  • python -m pyisy CLI: --tls-ver removed. The smoke-test entry point now always uses tls_ver="auto" and passes verify_ssl=False explicitly as the canonical example for new consumers. (#494, #499)
  • SSL/TLS handshake failures now raise ISYConnectionError on every code path. aiohttp.ClientConnectorSSLError is a subclass of ClientOSError, so the existing catch-all branch silently classified handshake failures as a generic DEBUG ISY not ready or closed connection. and returned None on the retry path. SSL failures are non-recoverable config mismatches (controller pinned below the auto-floor TLS 1.2, or verify_ssl=True against a self-signed cert) — retrying won't help. Connection.request() now always raises ISYConnectionError("SSL/TLS error: …") with the original aiohttp error in __cause__. The ISY.initialize() exception type is unchanged; only the message is more specific. (#507)
  • HTTPS to ISY-994 on modern OpenSSL: legacy-renegotiation compat enabled on demand. ISY-994 firmware's TLS stack pre-dates RFC 5746, so OpenSSL 3.x (Ubuntu 22.04+ / RHEL 9 / current Python distros) refuses the handshake with UNSAFE_LEGACY_RENEGOTIATION_DISABLED even after a successful version+cipher negotiation — without this, the auto-TLS modernization can't actually reach an ISY-994 over HTTPS on a stock modern host. Connection.request() now flips OP_LEGACY_SERVER_CONNECT on the existing SSL context the first time the peer rejects the handshake with that specific failure, logs a one-time WARNING, and retries. Modern peers (eisy/Polisy IoX with TLS 1.3, or ISY-994 firmware that honors RFC 5746) never reach that branch and stay strict for the lifetime of the Connection. (#508)

Downstream HA Core follow-up (with the pyisy version-bump PR)

  • Drop the TLS-version dropdown from the isy994 config flow; let tls_ver default to "auto".
  • Add a "Verify SSL certificate" toggle (default off), passed through to ISY(verify_ssl=...).
  • Distinguish TLS failures from generic connect errors in the config flow by matching on __cause__:
    except ISYConnectionError as err:
        if isinstance(err.__cause__, aiohttp.ClientSSLError):
            errors["base"] = "ssl_error"
        else:
            errors["base"] = "cannot_connect"

🐛 Bug fixes

  • Connection.get_description() no longer crashes with IndexError. Connection.request() was deriving its debug-log endpoint via url.split("rest", 1)[1], which raised on /desc. Switched to str.partition with a fallback to the full URL when the separator is absent. (#489, fixes #488)
  • Program enable/disable websocket events now actually update pobj.enabled. Programs.update_received was substring-matching XML_ON = "<on />" against xmldoc.toxml(), but minidom collapses self-closing tags (<on /><on/>), so the toggle branch was dead code. Switched to xmldoc.getElementsByTagName("on" / "off"). (#490, fixes #487)
  • Variable.protocol returns the correct value. Variables.parse() stores _type as int while VAR_INTEGER/VAR_STATE are "1"/"2", so the equality check was always False and every variable reported PROTO_STATE_VAR. Cast _type to str at the comparison site. (#491, fixes #485)
  • Variables name-based lookup paths fixed. __getitem__ raised TypeError on every name-based access (iterated dict[int, str] and tried to unpack ints into pairs); get_by_name substring-matched against a 3-tuple's repr and leaked StopIteration on no-match. Both switched to linear scan with name equality (matching #445). get_by_name now returns None on miss. HA Core impact: none — the integration looks up variables by integer, never by name. (#492, fixes #486)
  • Three small quirks surfaced by the test suite (#493):
    • Nodes.parse() re-update path was missing await on Node.update(xmldoc=feature), so the coroutine was silently dropped and re-encountered nodes never refreshed status from new XML.
    • NodeIterator was missing __iter__, so for x in iter(nodes) and reversed(nodes) raised TypeError. Fix covers Programs (which aliases the same class) too.
    • Programs.__getitem__ raised KeyError on unrecognized keys despite the -> ... | None signature; now returns None like every other miss path.
  • Empty optional endpoints on a factory-reset ISY-994 no longer spam ERROR logs. A controller with no variables and no networking resources configured returns 404 for /rest/vars/definitions/{1,2} and /rest/networking/resources — and its keep-alive framing desyncs after the 404 so the next request reuses the socket and aiohttp's parser raises ClientResponseError("Expected HTTP/, RTSP/ or ICE/:") reading the prior response's HTML body. get_variable_defs() now passes ok404=True; Variables.parse_definitions treats "" the same as None; and request() absorbs ClientResponseError as benign whenever the caller already opted into ok404=True. Functionality was already correct; this is purely log-level noise reduction. (#506)

🧪 Tests / CI

  • Offline pytest suite — 443 tests, ~86% line coverage. Covers the XML parsers, the Connection request layer, the full ISY.initialize() lifecycle, every per-node and per-program action method HA's isy994 integration calls, climate / lock / Z-Wave, the websocket router and lifecycle (replaying 41 sanitized real-controller events), the helpers / event emitter / datetime parsers, and a CLI smoke. Suite is fully offline — tests.conftest.FakeConnection serves canned, anonymized XML from tests/fixtures/. Wires a tests job into CI on Python 3.11 and 3.14. (#484)

📦 Dependencies / tooling

  • Upgrade GitHub Actions to the Node.js 24 runtime ahead of the June 2026 deprecation deadline (checkout v6, upload-artifact v7, download-artifact v8). (#483)
  • Align .vscode settings/extensions with the modernized ruff tooling — drop ms-python.* and python.linting.*/python.formatting.* keys; add charliermarsh.ruff as default formatter; recommend pylance, prettier, code-spell-checker, autodocstring. (#498)
  • Bump sphinx >=7.4.7>=9.0.4 (#496); pre-commit >=4.3.0>=4.6.0 (#495); routine pre-commit autoupdate (prettier v3.6.2 → v3.8.3, #497).
  • Bump test-only deps to current majors: pytest >=8.0>=9.0.3 (#505), pytest-cov >=5.0>=7.1.0 (#504), pytest-asyncio >=0.23>=1.3.0 (#502), syrupy >=4.6>=5.1.0 (#501), aioresponses >=0.7>=0.7.8 (#503).
  • Internal: migrate the remaining clean asyncio.gather fan-outs in ISY.initialize() and Programs.update() to asyncio.TaskGroup for structured-concurrency error propagation; no behavior change. (#500)

Full changelog: v3.5.1...v3.6.1
V3.6.0 yanked for shipping tests directory in wheel

v3.5.1 — 2026 Maintenance Release and bump Python to 3.11+

Choose a tag to compare

@shbatm shbatm released this 02 May 18:44
ad0d74e

Heads-up: this release drops Python 3.9 and 3.10 support and removes two install-time dependencies. Test in a 3.11+ environment before upgrading production consumers.

⚠️ Breaking changes

  • Python 3.11+ is now required. requires-python was bumped from >=3.9 to >=3.11 to match current revision used on eisy/PG3x (acknowledging that Home Assistant is on >=3.14.2 at this time), and install_requires no longer pulls in python-dateutil or requests — both were unused at runtime once the API became fully aiohttp-based. Update your environment before upgrading. (#474)

Bug fixes

  • Retry on spurious 404 from command-issuing endpoints. ISY-994 firmware emits transient HTTP 404s on /rest/nodes/.../cmd/... when its Insteon mesh is overwhelmed, which previously dropped commands silently with "ISY Reported an Invalid Command Received" log spam. The 404 branch in Connection.request() now feeds into the existing retry/backoff loop (5 retries, ~3.4s budget) for command-issuing callers (NodeBase.send_cmd / disable / enable / rename, Folder.send_cmd, Variable.set_value, ISY.query, ISY.send_x10_cmd). Read paths and the initial-connect fail-fast path are unchanged. No-op on eisy/Polisy, where the firmware queue-manages the mesh internally instead of returning 404s. (#469, fixes long-running #184)
  • Fail fast on partial / missing setup data. When the ISY is still booting it returns malformed XML or empty responses to the parallel setup REST calls fired by ISY.initialize(). Previously two of the parsers (Programs, NetworkResources) silently swallowed the error and the consumer crashed later (e.g. HA's _categorize_programs hitting len(None)). Both now raise ISYResponseParseError like the other parsers, and ISY.initialize() fails fast when status, time, nodes, or programs come back None, so HA Core's async_setup_entry can convert that into ConfigEntryNotReady and retry instead of mounting a half-loaded controller. networking is intentionally not in the fail-fast set (per #457, a None from get_network() is a normal "no RES.CFG" capability state). (#481, closes #297)
  • Fix python -m pyisy ... crashing immediately with TypeError: Event.wait() missing 1 required positional argument: 'self'. The smoke-test entry point was calling Event.wait() on the class instead of an instance — regression from #408. (#478)
  • Tolerate 404 and malformed XML on /rest/networking/resources so eisy units without the networking module enabled now initialize cleanly. Thanks @funkadelic for the first contribution. (#457)
  • Program last_run / last_finished from the websocket event stream now populate correctly. The new internal datetime parser was rejecting the trailing whitespace that dateutil's parser had silently tolerated. (#474)

Refactors and type tightening

  • Migrate event-stream status constants to StrEnum. New EventStreamStatus(StrEnum) in pyisy.constants is the canonical home for the lifecycle values published on ISY.connection_events. The existing ES_* module-level names are now aliases bound to the enum members. StrEnum members compare equal to their string values and pass isinstance(_, str), so HA Core's isy994 integration and any other consumer that imports ES_CONNECTED (or compares to the raw string) keeps working unchanged. (#480, closes #476)
  • Drop redundant _LOGGER.error before raise. Every site that logged an ERROR immediately before raising the same exception was double-reporting the failure: the standalone log line lacked a traceback, while the caller (or HA's setup pipeline) would log the propagated exception with full context anyway. When the caller intentionally caught it (e.g., HA ConfigEntryNotReady retry), the standalone ERROR was just visible noise for a recoverable condition. Cleaned up across all XML parsers (Clock, Configuration, Variables, Nodes, NodeServers, Node, NodeBase) and two Connection sites (test_connection, HTTP 401 in request). Each raise now embeds the source location in the exception message so it stays visible in the traceback. No call sites change; exception types and __cause__ chains are identical. (#482)
  • Replace dateutil.parser.parse with pyisy.helpers.parse_isy_datetime, an internal parser that walks the four ISY datetime formats (YYYY/MM/DD HH:MM:SS, AM/PM long form, YYYYMMDD HH:MM:SS, YYMMDD HH:MM:SS) before falling back to ISO 8601, and returns EMPTY_TIME instead of raising. (#474)
  • Tighten the XML helper return types (value_from_xml, attr_from_xml, attr_from_element, value_from_nested_xmlstr | None), fix Optional fields on NodeProperty and EventListener, complete annotations on events.tcpsocket.EventStream and on the three node update() overrides, and switch ZWaveProperties.from_xml to typing.Self. (#479)
  • 3.11 cleanups in the existing code paths: catch the unified TimeoutError rather than the deprecated asyncio.TimeoutError alias in connection.py and events/websocket.py; pass strict=True to the zip() in node_servers.py. (#474)

Documentation

  • Modernize the README and deprecate CHANGELOG.md in favor of auto-generated GitHub release notes. (#471)
  • Drop requests / dateutil from docs/conf.py and docs/index.rst; replace the async_timeout example in docs/quickstart.rst with the stdlib asyncio.timeout. (#474)

Development

  • Devcontainer: switch to the current mcr.microsoft.com/vscode/devcontainers/python namespace, bump to the 3.11-bookworm variant, modernize the ENV syntax, and drop the unmaintained IntelliCode extension recommendation. (#473, #475, #478)
  • Add CLAUDE.md guidance for Claude Code, including a note on the retry404 flag's intended scope. (#467, #469)
  • Modernize CI tooling and consolidate dev dependencies. (#468)
  • Bump Sphinx to >=7.4.7 (#470) and routine pre-commit hook updates (#446#451).

New contributors

Full changelog: v3.4.1...v3.5.1

v3.5.0b0 - [beta] Maintenance Release and bump Python to 3.11

Choose a tag to compare

@shbatm shbatm released this 02 May 15:17
802ed71

Heads-up: this release drops Python 3.9 and 3.10 support and removes two install-time dependencies. Test in a 3.11+ environment before upgrading production consumers.

⚠️ Breaking changes

  • Python 3.11+ is now required. requires-python was bumped from >=3.9 to >=3.11, and install_requires no longer pulls in python-dateutil or requests — both were unused at runtime once the API became fully aiohttp-based. Update your environment before upgrading. (#474)

Bug fixes

  • Fix python -m pyisy ... crashing immediately with TypeError: Event.wait() missing 1 required positional argument: 'self'. The smoke-test entry point was calling Event.wait() on the class instead of an instance — regression from #408. (#478)
  • Tolerate 404 and malformed XML on /rest/networking/resources so eisy units without the networking module enabled now initialize cleanly. Thanks @funkadelic for the first contribution. (#457)
  • Program last_run / last_finished from the websocket event stream now populate correctly. The new internal datetime parser was rejecting the trailing whitespace that dateutil's parser had silently tolerated. (#474)

Refactors and type tightening

  • Replace dateutil.parser.parse with pyisy.helpers.parse_isy_datetime, an internal parser that walks the four ISY datetime formats (YYYY/MM/DD HH:MM:SS, AM/PM long form, YYYYMMDD HH:MM:SS, YYMMDD HH:MM:SS) before falling back to ISO 8601, and returns EMPTY_TIME instead of raising. (#474)
  • Tighten the XML helper return types (value_from_xml, attr_from_xml, attr_from_element, value_from_nested_xmlstr | None), fix Optional fields on NodeProperty and EventListener, complete annotations on events.tcpsocket.EventStream and on the three node update() overrides, and switch ZWaveProperties.from_xml to typing.Self. (#479)
  • 3.11 cleanups in the existing code paths: catch the unified TimeoutError rather than the deprecated asyncio.TimeoutError alias in connection.py and events/websocket.py; pass strict=True to the zip() in node_servers.py. (#474)

Documentation

  • Modernize the README and deprecate CHANGELOG.md in favor of auto-generated GitHub release notes. (#471)
  • Drop requests / dateutil from docs/conf.py and docs/index.rst; replace the async_timeout example in docs/quickstart.rst with the stdlib asyncio.timeout. (#474)

Development

  • Devcontainer: switch to the current mcr.microsoft.com/vscode/devcontainers/python namespace, bump to the 3.11-bookworm variant, modernize the ENV syntax, and drop the unmaintained IntelliCode extension recommendation. (#473, #475, #478)
  • Add CLAUDE.md guidance for Claude Code. (#467)
  • Modernize CI tooling and consolidate dev dependencies. (#468)
  • Bump Sphinx to >=7.4.7 (#470) and routine pre-commit hook updates (#446#451).

New contributors

Full changelog: v3.4.1...v3.5.0a1

v3.4.1

Choose a tag to compare

@bdraco bdraco released this 02 May 22:05
68cac54

What's Changed

  • Remove name-based node indexing (names may not be unique) by @bdraco in #445

Full Changelog: v3.4.0...v3.4.1

v3.4.0

Choose a tag to compare

@bdraco bdraco released this 30 Mar 18:49
cf5a21f

What's Changed

Full Changelog: v3.3.0...v3.4.0

v3.3.0

Choose a tag to compare

@bdraco bdraco released this 30 Mar 18:14
39e9513

What's Changed

Full Changelog: v3.2.0...v3.3.0

v3.2.0

Choose a tag to compare

@bdraco bdraco released this 30 Mar 03:49
a5d9bae

What's Changed

Full Changelog: v3.1.15...v3.2.0

v3.1.15

Choose a tag to compare

@bdraco bdraco released this 30 Mar 00:05
b996113

What's Changed

  • Handle unsub message with no stream id by @shbatm in #387
  • Drop Python 3.8 support by @bdraco in #400
  • Add Insteon Type 0x0F to Lock Types by @shbatm in #395
  • Update publish workflow to use Trusted Publishing by @bdraco in #403
  • Make CONFIG_PORTAL key optional by @bdraco in #404

Full Changelog: v3.1.14...v3.1.15

[2.1.7] Maintenance Release for V2

Choose a tag to compare

@shbatm shbatm released this 21 Mar 11:23
63f846e

What's Changed

Full Changelog: 2.1.6...2.1.7

v3.1.14

Choose a tag to compare

@shbatm shbatm released this 25 Feb 11:44
27c0829

What's Changed

  • Add Z-Wave Lock Commands and Fix ZMatter URL by @shbatm in #383

Full Changelog: v3.1.13...v3.1.14