Releases: automicus/PyISY
Release list
v3.6.1 - TLS modernization + offline test suite
Heads-up: the default TLS configuration changes from a pinned
1.1to"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"; numerictls_vervalues deprecated.get_sslcontextswitches tossl.PROTOCOL_TLS_CLIENTand the"auto"default setsminimum_version=TLSv1_2with 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. Numerictls_ver(1.1/1.2/1.3) still works behind aDeprecationWarning; pin only when an ISY-994 has been manually downgraded below TLS 1.2. RemovesPROTOCOL_TLSv1_Xdeprecation noise on Python 3.12+. (#494, #499) - New
verify_ssl: bool = Falsekwarg onISY,Connection,WebSocketClient, andget_sslcontext.False(default) preservesCERT_NONEfor self-signed controllers;Trueopts intoCERT_REQUIRED+check_hostnamefor users with a properly signed certificate. (#494, #499) python -m pyisyCLI:--tls-verremoved. The smoke-test entry point now always usestls_ver="auto"and passesverify_ssl=Falseexplicitly as the canonical example for new consumers. (#494, #499)- SSL/TLS handshake failures now raise
ISYConnectionErroron every code path.aiohttp.ClientConnectorSSLErroris a subclass ofClientOSError, so the existing catch-all branch silently classified handshake failures as a genericDEBUG ISY not ready or closed connection.and returnedNoneon the retry path. SSL failures are non-recoverable config mismatches (controller pinned below the auto-floor TLS 1.2, orverify_ssl=Trueagainst a self-signed cert) — retrying won't help.Connection.request()now always raisesISYConnectionError("SSL/TLS error: …")with the originalaiohttperror in__cause__. TheISY.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_DISABLEDeven 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 flipsOP_LEGACY_SERVER_CONNECTon 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 theConnection. (#508)
Downstream HA Core follow-up (with the pyisy version-bump PR)
- Drop the TLS-version dropdown from the
isy994config flow; lettls_verdefault 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 withIndexError.Connection.request()was deriving its debug-log endpoint viaurl.split("rest", 1)[1], which raised on/desc. Switched tostr.partitionwith 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_receivedwas substring-matchingXML_ON = "<on />"againstxmldoc.toxml(), butminidomcollapses self-closing tags (<on />→<on/>), so the toggle branch was dead code. Switched toxmldoc.getElementsByTagName("on" / "off"). (#490, fixes #487) Variable.protocolreturns the correct value.Variables.parse()stores_typeasintwhileVAR_INTEGER/VAR_STATEare"1"/"2", so the equality check was alwaysFalseand every variable reportedPROTO_STATE_VAR. Cast_typetostrat the comparison site. (#491, fixes #485)- Variables name-based lookup paths fixed.
__getitem__raisedTypeErroron every name-based access (iterateddict[int, str]and tried to unpack ints into pairs);get_by_namesubstring-matched against a 3-tuple'sreprand leakedStopIterationon no-match. Both switched to linear scan with name equality (matching #445).get_by_namenow returnsNoneon 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 missingawaitonNode.update(xmldoc=feature), so the coroutine was silently dropped and re-encountered nodes never refreshed status from new XML.NodeIteratorwas missing__iter__, sofor x in iter(nodes)andreversed(nodes)raisedTypeError. Fix coversPrograms(which aliases the same class) too.Programs.__getitem__raisedKeyErroron unrecognized keys despite the-> ... | Nonesignature; now returnsNonelike 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 raisesClientResponseError("Expected HTTP/, RTSP/ or ICE/:")reading the prior response's HTML body.get_variable_defs()now passesok404=True;Variables.parse_definitionstreats""the same asNone; andrequest()absorbsClientResponseErroras benign whenever the caller already opted intook404=True. Functionality was already correct; this is purely log-level noise reduction. (#506)
🧪 Tests / CI
- Offline
pytestsuite — 443 tests, ~86% line coverage. Covers the XML parsers, theConnectionrequest layer, the fullISY.initialize()lifecycle, every per-node and per-program action method HA'sisy994integration 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.FakeConnectionserves canned, anonymized XML fromtests/fixtures/. Wires atestsjob 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 (
checkoutv6,upload-artifactv7,download-artifactv8). (#483) - Align
.vscodesettings/extensions with the modernized ruff tooling — dropms-python.*andpython.linting.*/python.formatting.*keys; addcharliermarsh.ruffas default formatter; recommendpylance,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 (prettierv3.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.gatherfan-outs inISY.initialize()andPrograms.update()toasyncio.TaskGroupfor 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+
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-pythonwas bumped from>=3.9to>=3.11to match current revision used on eisy/PG3x (acknowledging that Home Assistant is on>=3.14.2at this time), andinstall_requiresno longer pulls inpython-dateutilorrequests— 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 inConnection.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_programshittinglen(None)). Both now raiseISYResponseParseErrorlike the other parsers, andISY.initialize()fails fast whenstatus,time,nodes, orprogramscome backNone, so HA Core'sasync_setup_entrycan convert that intoConfigEntryNotReadyand retry instead of mounting a half-loaded controller.networkingis intentionally not in the fail-fast set (per #457, aNonefromget_network()is a normal "no RES.CFG" capability state). (#481, closes #297) - Fix
python -m pyisy ...crashing immediately withTypeError: Event.wait() missing 1 required positional argument: 'self'. The smoke-test entry point was callingEvent.wait()on the class instead of an instance — regression from #408. (#478) - Tolerate 404 and malformed XML on
/rest/networking/resourcesso eisy units without the networking module enabled now initialize cleanly. Thanks @funkadelic for the first contribution. (#457) - Program
last_run/last_finishedfrom 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. NewEventStreamStatus(StrEnum)inpyisy.constantsis the canonical home for the lifecycle values published onISY.connection_events. The existingES_*module-level names are now aliases bound to the enum members.StrEnummembers compare equal to their string values and passisinstance(_, str), so HA Core'sisy994integration and any other consumer that importsES_CONNECTED(or compares to the raw string) keeps working unchanged. (#480, closes #476) - Drop redundant
_LOGGER.errorbeforeraise. 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., HAConfigEntryNotReadyretry), 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 twoConnectionsites (test_connection, HTTP 401 inrequest). Eachraisenow 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.parsewithpyisy.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 returnsEMPTY_TIMEinstead of raising. (#474) - Tighten the XML helper return types (
value_from_xml,attr_from_xml,attr_from_element,value_from_nested_xml→str | None), fix Optional fields onNodePropertyandEventListener, complete annotations onevents.tcpsocket.EventStreamand on the three nodeupdate()overrides, and switchZWaveProperties.from_xmltotyping.Self. (#479) - 3.11 cleanups in the existing code paths: catch the unified
TimeoutErrorrather than the deprecatedasyncio.TimeoutErroralias inconnection.pyandevents/websocket.py; passstrict=Trueto thezip()innode_servers.py. (#474)
Documentation
- Modernize the README and deprecate
CHANGELOG.mdin favor of auto-generated GitHub release notes. (#471) - Drop
requests/dateutilfromdocs/conf.pyanddocs/index.rst; replace theasync_timeoutexample indocs/quickstart.rstwith the stdlibasyncio.timeout. (#474)
Development
- Devcontainer: switch to the current
mcr.microsoft.com/vscode/devcontainers/pythonnamespace, bump to the 3.11-bookworm variant, modernize theENVsyntax, and drop the unmaintained IntelliCode extension recommendation. (#473, #475, #478) - Add
CLAUDE.mdguidance for Claude Code, including a note on theretry404flag'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
- @funkadelic in #457
Full changelog: v3.4.1...v3.5.1
v3.5.0b0 - [beta] Maintenance Release and bump Python to 3.11
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-pythonwas bumped from>=3.9to>=3.11, andinstall_requiresno longer pulls inpython-dateutilorrequests— 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 withTypeError: Event.wait() missing 1 required positional argument: 'self'. The smoke-test entry point was callingEvent.wait()on the class instead of an instance — regression from #408. (#478) - Tolerate 404 and malformed XML on
/rest/networking/resourcesso eisy units without the networking module enabled now initialize cleanly. Thanks @funkadelic for the first contribution. (#457) - Program
last_run/last_finishedfrom 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.parsewithpyisy.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 returnsEMPTY_TIMEinstead of raising. (#474) - Tighten the XML helper return types (
value_from_xml,attr_from_xml,attr_from_element,value_from_nested_xml→str | None), fix Optional fields onNodePropertyandEventListener, complete annotations onevents.tcpsocket.EventStreamand on the three nodeupdate()overrides, and switchZWaveProperties.from_xmltotyping.Self. (#479) - 3.11 cleanups in the existing code paths: catch the unified
TimeoutErrorrather than the deprecatedasyncio.TimeoutErroralias inconnection.pyandevents/websocket.py; passstrict=Trueto thezip()innode_servers.py. (#474)
Documentation
- Modernize the README and deprecate
CHANGELOG.mdin favor of auto-generated GitHub release notes. (#471) - Drop
requests/dateutilfromdocs/conf.pyanddocs/index.rst; replace theasync_timeoutexample indocs/quickstart.rstwith the stdlibasyncio.timeout. (#474)
Development
- Devcontainer: switch to the current
mcr.microsoft.com/vscode/devcontainers/pythonnamespace, bump to the 3.11-bookworm variant, modernize theENVsyntax, and drop the unmaintained IntelliCode extension recommendation. (#473, #475, #478) - Add
CLAUDE.mdguidance 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
- @funkadelic in #457
Full changelog: v3.4.1...v3.5.0a1
v3.4.1
v3.4.0
v3.3.0
v3.2.0
What's Changed
- Enable ruff SIM102 by @bdraco in #407
- Enable ruff ASYNC110 by @bdraco in #408
- Enable ruff PERF401 by @bdraco in #409
- Enable ruff TRY401 by @bdraco in #410
- Enable ruff SIM201 by @bdraco in #411
- Enable ruff RSE102 by @bdraco in #412
- Enable ruff SIM108 by @bdraco in #413
- Enable ruff C419 by @bdraco in #414
- Enable ruff RET504 by @bdraco in #415
- Enable ruff RET503 by @bdraco in #416
- Enable ruff TRY400 by @bdraco in #417
- Enable ruff PLW2901 by @bdraco in #418
- Enable ruff PERF203 by @bdraco in #419
- Enable ruff RUF013 by @bdraco in #420
- Enable ruff SIM118 by @bdraco in #421
- Enable ruff G201 by @bdraco in #422
- Use identity checks for WSMsgType by @bdraco in #423
- Enable ruff RUF006 by @bdraco in #424
- Fix calls to loop.create_task that do not hold a strong reference by @bdraco in #425
- Refactor node lookup to reduce time complexity by @bdraco in #405
- Cleanups to nodes group by @bdraco in #406
- Suppress TRY400 for WebSocket ClientConnectorError by @bdraco in #429
- Refactor programs to reduce time complexity by @bdraco in #427
- Handle KeyError for new programs by @bdraco in #430
- Add typing to the WebSocket event handler by @bdraco in #431
- Add typing to folders by @bdraco in #432
- Rework WebSocket reconnect delay to use a TimerHandle by @bdraco in #433
Full Changelog: v3.1.15...v3.2.0
v3.1.15
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
What's Changed
- Bump pylint from 2.13.8 to 2.15.2 by @dependabot in #293
- Bump pyupgrade from 2.32.0 to 2.37.3 by @dependabot in #284
- Bump black from 22.3.0 to 22.8.0 by @dependabot in #291
- Bump codespell from 2.1.0 to 2.2.1 by @dependabot in #289
- Bump flake8 from 4.0.1 to 5.0.4 by @dependabot in #288
- Bump codespell from 2.2.1 to 2.2.2 by @dependabot in #303
- Bump flake8 from 5.0.4 to 6.0.0 by @dependabot in #310
- Bump pylint from 2.15.2 to 2.15.9 by @dependabot in #321
- Bump black from 22.8.0 to 22.12.0 by @dependabot in #317
- Bump pyupgrade from 2.37.3 to 3.3.1 by @dependabot in #316
- Bump isort from 5.10.1 to 5.11.3 by @dependabot in #320
- V2 EOL Notice by @shbatm in #385
- Move UNSUB message inside of try catch by @shbatm in #386
Full Changelog: 2.1.6...2.1.7