Skip to content

Commit

Permalink
Improved error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
jnimmo committed Aug 3, 2023
1 parent edfcb25 commit ca270aa
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 61 deletions.
1 change: 0 additions & 1 deletion examples/cli_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ async def main(loop):
device_type=args.device,
)
else:

controller = IntesisHome(
args.user,
args.password,
Expand Down
1 change: 0 additions & 1 deletion examples/dhw_aquarea_domoticz.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,6 @@ def aquarea_to_domoticz(


async def main(loop):

username = "xxxxxx"
password = "yyyyyyyyy"
idd = "zzzzzzzz"
Expand Down
15 changes: 7 additions & 8 deletions pyintesishome/intesisbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def __init__(
loop=None,
websession=None,
device_type=DEVICE_INTESISHOME,
):
) -> None:
"""Initialize IntesisBox controller."""
# Select correct API for device type
self._username = username
Expand All @@ -50,7 +50,7 @@ def __init__(
self._error_message = None
self._web_session = websession
self._own_session = False
self._controller_id = username
self._controller_id = None
self._controller_name = username
self._writer: StreamWriter = None
self._reader: StreamReader = None
Expand Down Expand Up @@ -87,8 +87,7 @@ async def _send_command(self, command: str):
)
except asyncio.TimeoutError:
print("oops took longer than 5s!")
# need to close the connection as device not responding due to hung state
self._writer.write.close()
await self.stop()
except OSError as exc:
_LOGGER.error("%s Exception. %s / %s", type(exc), exc.args, exc)

Expand All @@ -106,8 +105,8 @@ async def _data_received(self):
_LOGGER.debug("Resolving set_value's await")
self._received_response.set()
except IncompleteReadError:
_LOGGER.info(
"pyIntesisHome lost connection to the %s server.", self._device_type
_LOGGER.debug(
"pyIntesisHome lost connection to the %s server", self._device_type
)
except asyncio.CancelledError:
pass
Expand All @@ -117,7 +116,7 @@ async def _data_received(self):
OSError,
) as exc:
_LOGGER.error(
"pyIntesisHome lost connection to the %s server. Exception: %s",
"PyIntesisHome lost connection to the %s server. Exception: %s",
self._device_type,
exc,
)
Expand Down Expand Up @@ -519,7 +518,7 @@ def controller_id(self) -> str:
"""Returns an account/device identifier - Serial, MAC or username."""
if self._controller_id:
return self._controller_id.lower()
return None
raise ValueError("Controller ID has not been set yet")

@property
def name(self) -> str:
Expand Down
3 changes: 2 additions & 1 deletion pyintesishome/intesishome.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def __init__(
self._cmd_server = None
self._cmd_server_port = None
self._auth_token = None
self._controller_id = username

async def _parse_response(self, decoded_data):
_LOGGER.debug("%s API Received: %s", self._device_type, decoded_data)
Expand Down Expand Up @@ -151,7 +152,7 @@ async def poll_status(self, sendcallback=False):
) as resp:
status_response = await resp.json(content_type=None)
_LOGGER.debug(status_response)
except (aiohttp.client_exceptions.ClientConnectorError) as exc:
except aiohttp.client_exceptions.ClientConnectorError as exc:
raise IHConnectionError from exc
except (aiohttp.client_exceptions.ClientError, socket.gaierror) as exc:
self._error_message = f"Error connecting to {self._device_type} API: {exc}"
Expand Down
135 changes: 87 additions & 48 deletions pyintesishome/intesishomelocal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import asyncio
import logging
from json import JSONDecodeError

import aiohttp

Expand All @@ -25,7 +26,7 @@
class IntesisHomeLocal(IntesisBase):
"""pyintesishome local class."""

def __init__(self, host, username, password, loop=None, websession=None):
def __init__(self, host, username, password, loop=None, websession=None) -> None:
"""Constructor"""
device_type = DEVICE_INTESISHOME_LOCAL
self._session_id: str = ""
Expand Down Expand Up @@ -70,7 +71,7 @@ async def _run_updater(self):
self._update_device_state(self._device_id, uid, value)

await self._send_update_callback(self._device_id)
except (IHConnectionError) as exc:
except IHConnectionError as exc:
_LOGGER.error("Error during updater task: %s", exc)
await asyncio.sleep(self._scan_interval)
except asyncio.CancelledError:
Expand All @@ -83,14 +84,35 @@ async def _authenticate(self) -> bool:
"command": LOCAL_CMD_LOGIN,
"data": {"username": self._username, "password": self._password},
}
async with self._web_session.post(
f"http://{self._host}/api.cgi", json=payload
) as response:
if response.status != 200:
raise IHConnectionError("HTTP response status is unexpected (not 200)")
json_response = await response.json()
self._session_id = json_response["data"].get("id").get("sessionID")
_LOGGER.debug("Authenticated with new session ID %s", self._session_id)
try:
async with self._web_session.post(
f"http://{self._host}/api.cgi", json=payload
) as response:
if response.status != 200:
raise IHConnectionError(
"HTTP response status is unexpected (not 200)"
)
json_response = await response.json()
# Check if the response has the expected format
if (
"data" in json_response
and "id" in json_response["data"]
and "sessionID" in json_response["data"]["id"]
):
self._session_id = json_response["data"]["id"]["sessionID"]
_LOGGER.debug(
"Authenticated with new session ID %s", self._session_id
)
else:
_LOGGER.error("Unexpected response format during authentication")
except (
aiohttp.ClientConnectionError,
aiohttp.ClientResponseError,
aiohttp.ClientPayloadError,
aiohttp.ContentTypeError,
JSONDecodeError,
) as exception:
_LOGGER.error("Error during authentication: %s", str(exception))

async def _request(self, command: str, **kwargs) -> dict:
"""Make a request."""
Expand All @@ -105,7 +127,9 @@ async def _request(self, command: str, **kwargs) -> dict:
"command": command,
"data": {"sessionID": self._session_id, **kwargs},
}
_LOGGER.debug("Sending intesishome_local command %s to %s", command, self._host)
_LOGGER.debug(
"Sending intesishome_local command %s to %s", command, self._host
)
timeout = aiohttp.ClientTimeout(total=10)
json_response = {}
try:
Expand All @@ -116,9 +140,8 @@ async def _request(self, command: str, **kwargs) -> dict:
) as response:
if response.status != 200:
raise IHConnectionError(
"HTTP response status is unexpected for %s (got %s, want 200)",
self._host,
response.status,
f"HTTP response status is unexpected for {self._host}"
"(got {response.status}, want 200)"
)
json_response = await response.json()
except asyncio.exceptions.TimeoutError as exc:
Expand All @@ -127,7 +150,7 @@ async def _request(self, command: str, **kwargs) -> dict:
self._host,
exc,
)
except (aiohttp.ClientError) as exc:
except aiohttp.ClientError as exc:
_LOGGER.error(
"IntesisHome HTTP error for %s: %s",
self._host,
Expand All @@ -147,25 +170,30 @@ async def _request(self, command: str, **kwargs) -> dict:
# wonky, so log an error plus the entire response.
if json_response.get("success", False):
return json_response.get("data")
elif "error" in json_response:
if "error" in json_response:
error = json_response["error"]
if error.get("code") in [1, 5]:
self._session_id = ""
_LOGGER.debug("Request failed for %s (code=%s, message=%r). Clearing session key to force re-authentication",
self._host,
error.get("code"),
error.get("message"),
)
_LOGGER.debug(
"Request failed for %s (code=%s, message=%r)."
"Clearing session key to force re-authentication",
self._host,
error.get("code"),
error.get("message"),
)
else:
_LOGGER.debug("Request failed for %s (code=%s, message=%r). Error not handled.",
self._host,
error.get("code"),
error.get("message"),
)
else:
_LOGGER.debug("Request failed for %s - no 'success' or 'error' keys. json_response=%r",
_LOGGER.debug(
"Request failed for %s (code=%s, message=%r). Error not handled",
self._host,
json_response)
error.get("code"),
error.get("message"),
)
else:
_LOGGER.debug(
"Request failed for %s - no 'success' or 'error' keys. json_response=%r",
self._host,
json_response,
)

async def _request_value(self, name: str) -> dict:
"""Get entity value by uid."""
Expand Down Expand Up @@ -202,15 +230,15 @@ def _has_datapoint(self, datapoint: str):
async def connect(self):
"""Connect to the device and start periodic updater."""
await self.poll_status()
_LOGGER.debug("Successful authenticated and polled. Fetching Datapoints.")
_LOGGER.debug("Successful authenticated and polled. Fetching Datapoints")
await self.get_datapoints()
self._connected = True
_LOGGER.debug("Starting updater task.")
_LOGGER.debug("Starting updater task")
self._update_task = asyncio.create_task(self._run_updater())

async def stop(self):
"""Disconnect and stop periodic updater."""
_LOGGER.debug("Stopping updater task.")
_LOGGER.debug("Stopping updater task")
await self._cancel_task_if_exists(self._update_task)
self._connected = False

Expand All @@ -225,25 +253,36 @@ async def get_info(self) -> dict:

async def poll_status(self, sendcallback=False):
"""Get device info for setup purposes."""
await self._authenticate()
info = await self.get_info()
self._device_id = info["sn"]
self._controller_id = info["sn"].lower()
self._controller_name = f"{self._info['deviceModel']} ({info['ownSSID']})"
# Setup devices
self._devices[self._device_id] = {
"name": info["ownSSID"],
"widgets": [],
"model": info["deviceModel"],
}
try:
await self._authenticate()
info = await self.get_info()

# Extract device_id up to the first space, if there is a space
raw_id = info.get("sn")
if raw_id:
device_id, *_ = raw_id.split(" ")
self._device_id = device_id
self._controller_id = device_id.lower()

self._controller_name = (
f"{self._info.get('deviceModel')} ({info.get('ownSSID')})"
)
# Setup devices
self._devices[self._device_id] = {
"name": info.get("ownSSID"),
"widgets": [],
"model": info.get("deviceModel"),
}

await self.get_datapoints()
_LOGGER.debug(repr(self._devices))
await self.get_datapoints()
_LOGGER.debug(repr(self._devices))

self._update_device_state(self._device_id, "acStatus", info["acStatus"])
self._update_device_state(self._device_id, "acStatus", info.get("acStatus"))

if sendcallback:
await self._send_update_callback(str(self._device_id))
if sendcallback:
await self._send_update_callback(str(self._device_id))
except (IHConnectionError, KeyError) as exception:
_LOGGER.error("Error during polling status: %s", str(exception))

def get_mode_list(self, device_id) -> list:
"""Get possible entity modes."""
Expand Down
2 changes: 1 addition & 1 deletion requirements_test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# types-* that have versions roughly corresponding to the packages they
# contain hints for available should be kept in sync with them

codecov==2.1.12
codecov==2.1.13
mock-open==1.4.0
mypy==0.971
pre-commit==2.20.0
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

setup(
name="pyintesishome",
version="1.8.4",
version="1.8.5",
description="A python3 library for running asynchronus communications with IntesisHome Smart AC Controllers",
long_description=long_description,
long_description_content_type="text/markdown",
Expand Down

0 comments on commit ca270aa

Please sign in to comment.