diff --git a/pyhilo/const.py b/pyhilo/const.py index 9fc4241..82ff21f 100644 --- a/pyhilo/const.py +++ b/pyhilo/const.py @@ -12,7 +12,7 @@ TOKEN_EXPIRATION_PADDING: Final = 300 VERIFY: Final = True DEVICE_REFRESH_TIME: Final = 1800 -PYHILO_VERSION: Final = "2023.08.03" +PYHILO_VERSION: Final = "2023.11.01" # TODO: Find a way to keep previous line in sync with pyproject.toml automatically CONTENT_TYPE_FORM: Final = "application/x-www-form-urlencoded" @@ -170,6 +170,7 @@ "zigbee_channel", "zig_bee_pairing_activated", "gateway_asset_id", + "e_tag", ] HILO_LIST_ATTRIBUTES: Final = [ @@ -190,6 +191,10 @@ "Outlet": "Switch", "SmokeDetector": "Sensor", "Thermostat": "Climate", + "FloorThermostat": "Climate", + "Ccr": "Switch", + "Cee": "Switch", + "Thermostat24V": "Climate", "Tracker": "Sensor", } diff --git a/pyhilo/devices.py b/pyhilo/devices.py index e33d3b0..ff1ca6d 100644 --- a/pyhilo/devices.py +++ b/pyhilo/devices.py @@ -92,10 +92,18 @@ async def update(self) -> None: # Don't do anything with unpaired device for now. # self.devices.remove(device) - async def update_gateway(self) -> None: - gateway = await self._api.get_gateway(self.location_id) - LOG.debug(f"Generating device (gateway) {gateway}") - self.generate_device(gateway) + async def update_devicelist_from_signalr( + self, values: list[dict[str, Any]] + ) -> list[HiloDevice]: + new_devices = [] + for raw_device in values: + LOG.debug(f"Generating device {raw_device}") + dev = self.generate_device(raw_device) + if dev not in self.devices: + self.devices.append(dev) + new_devices.append(dev) + + return new_devices async def async_init(self) -> None: """Initialize the Hilo "manager" class.""" diff --git a/pyhilo/event.py b/pyhilo/event.py index 6207053..b3fc218 100644 --- a/pyhilo/event.py +++ b/pyhilo/event.py @@ -36,6 +36,7 @@ def __init__(self, **event: dict[str, Any]): if allowed_wH > 0: self.used_percentage = round(used_wH / allowed_wH * 100, 2) self.dict_items = [ + "event_id", "participating", "configurable", "period", diff --git a/pyhilo/websocket.py b/pyhilo/websocket.py index 6b7af7a..a49c900 100644 --- a/pyhilo/websocket.py +++ b/pyhilo/websocket.py @@ -7,7 +7,7 @@ from enum import IntEnum import json from os import environ -from typing import TYPE_CHECKING, Any, Callable, Dict, cast +from typing import TYPE_CHECKING, Any, Callable, Dict from aiohttp import ClientWebSocketResponse, WSMsgType from aiohttp.client_exceptions import ( @@ -159,34 +159,43 @@ def remove() -> None: return remove - async def _async_receive_json(self) -> dict[str, Any]: + async def _async_receive_json(self) -> list[Dict[str, Any]]: """Receive a JSON response from the websocket server.""" assert self._client - msg = await self._client.receive(300) - if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSED, WSMsgType.CLOSING): - LOG.error(f"Websocket: Received event to close connection: {msg.type}") + + response = await self._client.receive(300) + + if response.type in (WSMsgType.CLOSE, WSMsgType.CLOSED, WSMsgType.CLOSING): + LOG.error(f"Websocket: Received event to close connection: {response.type}") raise ConnectionClosedError("Connection was closed.") - if msg.type == WSMsgType.ERROR: - LOG.error(f"Websocket: Received error event, Connection failed: {msg.type}") + if response.type == WSMsgType.ERROR: + LOG.error( + f"Websocket: Received error event, Connection failed: {response.type}" + ) raise ConnectionFailedError - if msg.type != WSMsgType.TEXT: - LOG.error(f"Websocket: Received invalid message: {msg}") - raise InvalidMessageError(f"Received non-text message: {msg.type}") + if response.type != WSMsgType.TEXT: + LOG.error(f"Websocket: Received invalid message: {response}") + raise InvalidMessageError(f"Received non-text message: {response.type}") + messages: list[Dict[str, Any]] = [] try: - data = json.loads(msg.data[:-1]) + # Sometimes the http lib stacks multiple messages in the buffer, we need to split them to process. + received_messages = response.data.strip().split("\x1e") + for msg in received_messages: + data = json.loads(msg) + messages.append(data) except ValueError as v_exc: raise InvalidMessageError("Received invalid JSON") from v_exc except json.decoder.JSONDecodeError as j_exc: - LOG.error(f"Received invalid JSON: {msg.data}") + LOG.error(f"Received invalid JSON: {msg}") LOG.exception(j_exc) data = {} self._watchdog.trigger() - return cast(Dict[str, Any], data) + return messages async def _async_send_json(self, payload: dict[str, Any]) -> None: """Send a JSON message to the websocket server. @@ -314,12 +323,16 @@ async def async_listen(self) -> None: LOG.info("Websocket: Listen started.") try: while not self._client.closed: - message = await self._async_receive_json() - self._parse_message(message) + messages = await self._async_receive_json() + for msg in messages: + self._parse_message(msg) except ConnectionClosedError as err: LOG.error(f"Websocket: Closed while listening: {err}") LOG.exception(err) pass + except InvalidMessageError as err: + LOG.warning(f"Websocket: Received invalid json : {err}") + pass finally: LOG.info("Websocket: Listen completed; cleaning up") self._watchdog.cancel() @@ -332,7 +345,7 @@ async def async_reconnect(self) -> None: """Reconnect (and re-listen, if appropriate) to the websocket.""" LOG.warning("Websocket: Reconnecting") await self.async_disconnect() - await asyncio.sleep(1) + await asyncio.sleep(5) await self.async_connect() async def send_status(self) -> None: