From ed5c39f4319535e0cb54bfd64edb0ecf101b34b1 Mon Sep 17 00:00:00 2001 From: shbatm Date: Wed, 1 Feb 2023 19:18:24 +0000 Subject: [PATCH 1/3] Wait for ISY to be ready when retrying commands --- pyisy/__main__.py | 2 +- pyisy/connection.py | 72 ++++++++++++++++++++++++++++++++++----- pyisy/events/websocket.py | 2 +- pyisy/isy.py | 16 ++------- 4 files changed, 68 insertions(+), 24 deletions(-) diff --git a/pyisy/__main__.py b/pyisy/__main__.py index 565077f..835e42f 100644 --- a/pyisy/__main__.py +++ b/pyisy/__main__.py @@ -94,7 +94,7 @@ def system_status_handler(event: str) -> None: node_changed_subscriber = isy.nodes.status_events.subscribe( node_changed_handler ) - system_status_subscriber = isy.status_events.subscribe( + system_status_subscriber = isy.system_status.status_events.subscribe( system_status_handler ) while True: diff --git a/pyisy/connection.py b/pyisy/connection.py index a93b365..89d55a8 100644 --- a/pyisy/connection.py +++ b/pyisy/connection.py @@ -7,7 +7,12 @@ import aiohttp from .constants import ( + ATTR_ACTION, METHOD_GET, + SYSTEM_BUSY, + SYSTEM_IDLE, + SYSTEM_NOT_BUSY, + SYSTEM_STATUS, URL_CLOCK, URL_CONFIG, URL_DEFINITIONS, @@ -26,6 +31,7 @@ XML_TRUE, ) from .exceptions import ISYConnectionError, ISYInvalidAuthError +from .helpers import EventEmitter, value_from_xml from .logging import _LOGGER, enable_logging MAX_HTTPS_CONNECTIONS_ISY = 2 @@ -52,6 +58,46 @@ EMPTY_XML_RESPONSE = '' +class ISYSystemStatus: + """Event manager class for ISY System Status.""" + + _event = asyncio.Event() + _status = SYSTEM_IDLE + + def __init__(self): + """Initialize a system status class.""" + self.status_events = EventEmitter() + self._event.set() + + @property + def status(self): + """Return the system status property.""" + return self._status + + @status.setter + def status(self, val): + """Update the system status property.""" + self._status = val + if val == SYSTEM_BUSY: + self._event.clear() + elif val in (SYSTEM_IDLE, SYSTEM_NOT_BUSY): + self._event.set() + else: + raise ISYConnectionError("ISY status unknown") + + async def is_ready(self): + """Wait for the ISY to be ready.""" + return await self._event.wait() + + def change_received(self, xmldoc): + """Handle System Status events from an event stream message.""" + action = value_from_xml(xmldoc, ATTR_ACTION) + if not action or action not in SYSTEM_STATUS: + return + self.status = action + self.status_events.notify(action) + + class Connection: """Connection object to manage connection to and interaction with ISY.""" @@ -80,6 +126,7 @@ def __init__( self._tls_ver = tls_ver self.use_https = use_https self._url = f"http{'s' if self.use_https else ''}://{self._address}:{self._port}{self._webroot}" + self.system_status = ISYSystemStatus() self.semaphore = asyncio.Semaphore( MAX_HTTPS_CONNECTIONS_ISY if use_https else MAX_HTTP_CONNECTIONS_ISY @@ -170,7 +217,9 @@ async def request(self, url, retries=0, ok404=False, delay=0): "ISY Reported an Invalid Command Received %s", endpoint ) res.release() - return None + # ISY may report 404 error for valid command if busy + if self.system_status.status != SYSTEM_BUSY: + return None if res.status == HTTP_UNAUTHORIZED: _LOGGER.error("Invalid credentials provided for ISY connection.") res.release() @@ -201,13 +250,20 @@ async def request(self, url, retries=0, ok404=False, delay=0): if retries is None: raise ISYConnectionError() if retries < MAX_RETRIES: - _LOGGER.debug( - "Retrying ISY Request in %ss, retry %s.", - RETRY_BACKOFF[retries], - retries + 1, - ) - # sleep to allow the ISY to catch up - await asyncio.sleep(RETRY_BACKOFF[retries]) + if self.system_status.status == SYSTEM_BUSY: + _LOGGER.debug("ISY is busy, waiting for system to be ready") + try: + await asyncio.wait_for(self.system_status.is_ready(), timeout=5.0) + except asyncio.TimeoutError as exc: + raise ISYConnectionError() from exc + else: + _LOGGER.debug( + "Retrying ISY Request in %ss, retry %s.", + RETRY_BACKOFF[retries], + retries + 1, + ) + # sleep to allow the ISY to catch up + await asyncio.sleep(RETRY_BACKOFF[retries]) # recurse to try again retry_result = await self.request(url, retries + 1, ok404=ok404) return retry_result diff --git a/pyisy/events/websocket.py b/pyisy/events/websocket.py index 1fdf84c..3a61527 100644 --- a/pyisy/events/websocket.py +++ b/pyisy/events/websocket.py @@ -203,7 +203,7 @@ async def _route_message(self, msg): elif cntrl == "_3": # Node Changed/Updated self.isy.nodes.node_changed_received(xmldoc) elif cntrl == "_5": # System Status Changed - self.isy.system_status_changed_received(xmldoc) + self.isy.conn.system_status.change_received(xmldoc) elif cntrl == "_7": # Progress report, device programming event self.isy.nodes.progress_report_received(xmldoc) diff --git a/pyisy/isy.py b/pyisy/isy.py index c205d3b..ae0c567 100644 --- a/pyisy/isy.py +++ b/pyisy/isy.py @@ -6,7 +6,6 @@ from .configuration import Configuration from .connection import Connection from .constants import ( - ATTR_ACTION, CMD_X10, ES_CONNECTED, ES_RECONNECT_FAILED, @@ -14,14 +13,12 @@ ES_START_UPDATES, ES_STOP_UPDATES, PROTO_ISY, - SYSTEM_BUSY, - SYSTEM_STATUS, URL_QUERY, X10_COMMANDS, ) from .events.tcpsocket import EventStream from .events.websocket import WebSocketClient -from .helpers import EventEmitter, value_from_xml +from .helpers import EventEmitter from .logging import _LOGGER, enable_logging from .networking import NetworkResources from .nodes import Nodes @@ -112,8 +109,7 @@ def __init__( self.networking = None self._hostname = address self.connection_events = EventEmitter() - self.status_events = EventEmitter() - self.system_status = SYSTEM_BUSY + self.system_status = self.conn.system_status self.loop = asyncio.get_running_loop() self._uuid = None @@ -279,11 +275,3 @@ async def send_x10_cmd(self, address, cmd): _LOGGER.info("ISY Sent X10 Command: %s To: %s", cmd, address) else: _LOGGER.error("ISY Failed to send X10 Command: %s To: %s", cmd, address) - - def system_status_changed_received(self, xmldoc): - """Handle System Status events from an event stream message.""" - action = value_from_xml(xmldoc, ATTR_ACTION) - if not action or action not in SYSTEM_STATUS: - return - self.system_status = action - self.status_events.notify(action) From 67447cab10fb80a9e80fc588b95d88601e099eea Mon Sep 17 00:00:00 2001 From: shbatm Date: Wed, 1 Feb 2023 19:19:11 +0000 Subject: [PATCH 2/3] Mark as beta (revertable) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4791aa3..fd62ab3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools~=62.3", "wheel","setuptools_scm[toml]>=6.2",] build-backend = "setuptools.build_meta" [project] -name = "pyisy" +name = "pyisy-beta" description = "Python module to talk to ISY devices from UDI." license = {text = "Apache-2.0"} keywords = ["home", "automation", "isy", "isy994", "isy-994", "UDI", "polisy", "eisy"] From 6b1aaf84116da3da54954bcf9473df0fa84d3541 Mon Sep 17 00:00:00 2001 From: shbatm Date: Wed, 1 Feb 2023 19:34:55 +0000 Subject: [PATCH 3/3] Fix no-else-return in Nodes --- pyisy/nodes/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyisy/nodes/__init__.py b/pyisy/nodes/__init__.py index 1b7717d..b6b7e31 100755 --- a/pyisy/nodes/__init__.py +++ b/pyisy/nodes/__init__.py @@ -593,8 +593,7 @@ def get_by_id(self, address): i = self.addresses.index(address) except ValueError: return None - else: - return self.get_by_index(i) + return self.get_by_index(i) def get_by_index(self, i): """