Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wait for ISY to be ready when retrying commands #380

Open
wants to merge 3 commits into
base: v3.x.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyisy/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
72 changes: 64 additions & 8 deletions pyisy/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -52,6 +58,46 @@
EMPTY_XML_RESPONSE = '<?xml version="1.0" encoding="UTF-8"?>'


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."""

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyisy/events/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
16 changes: 2 additions & 14 deletions pyisy/isy.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,19 @@
from .configuration import Configuration
from .connection import Connection
from .constants import (
ATTR_ACTION,
CMD_X10,
ES_CONNECTED,
ES_RECONNECT_FAILED,
ES_RECONNECTING,
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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
3 changes: 1 addition & 2 deletions pyisy/nodes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down