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

Support new SignalR commands, device types and misc fixes #141

Merged
Merged
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
7 changes: 6 additions & 1 deletion pyhilo/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -170,6 +170,7 @@
"zigbee_channel",
"zig_bee_pairing_activated",
"gateway_asset_id",
"e_tag",
]

HILO_LIST_ATTRIBUTES: Final = [
Expand All @@ -190,6 +191,10 @@
"Outlet": "Switch",
"SmokeDetector": "Sensor",
"Thermostat": "Climate",
"FloorThermostat": "Climate",
"Ccr": "Switch",
"Cee": "Switch",
"Thermostat24V": "Climate",
"Tracker": "Sensor",
}

Expand Down
16 changes: 12 additions & 4 deletions pyhilo/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
1 change: 1 addition & 0 deletions pyhilo/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
45 changes: 29 additions & 16 deletions pyhilo/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand All @@ -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:
Expand Down