Skip to content

Commit

Permalink
Merge pull request #315 from nlz242/feature/SignalRMessages-and-misc-…
Browse files Browse the repository at this point in the history
…fixes

Support new SignalR commands, device types and misc fixes
  • Loading branch information
valleedelisle authored Nov 15, 2023
2 parents c8192a8 + a8413d4 commit 23c20b0
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 31 deletions.
55 changes: 34 additions & 21 deletions custom_components/hilo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
DOMAIN,
HILO_ENERGY_TOTAL,
LOG,
MIN_SCAN_INTERVAL,
)

DISPATCHER_TOPIC_WEBSOCKET_EVENT = "pyhilo_websocket_event"
Expand Down Expand Up @@ -126,6 +127,9 @@ async def async_setup_entry( # noqa: C901
current_options = {**entry.options}
log_traces = current_options.get(CONF_LOG_TRACES, DEFAULT_LOG_TRACES)
scan_interval = current_options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
scan_interval = (
scan_interval if scan_interval >= MIN_SCAN_INTERVAL else MIN_SCAN_INTERVAL
)
state_yaml = hass.config.path(DEFAULT_STATE_FILE)

websession = aiohttp_client.async_get_clientsession(hass)
Expand Down Expand Up @@ -211,10 +215,7 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry, api: API) -> None:
self.devices: Devices = Devices(api)
self._websocket_reconnect_task: asyncio.Task | None = None
self._update_task: asyncio.Task | None = None
self.invocations = {
0: self.subscribe_to_location,
1: self.subscribe_to_attributes,
}
self.invocations = {0: self.subscribe_to_location}
self.hq_plan_name = entry.options.get(CONF_HQ_PLAN_NAME, DEFAULT_HQ_PLAN_NAME)
self.appreciation = entry.options.get(
CONF_APPRECIATION_PHASE, DEFAULT_APPRECIATION_PHASE
Expand Down Expand Up @@ -260,6 +261,9 @@ async def on_websocket_event(self, event: WebsocketEvent) -> None:
for item in event.arguments[0]
)
if new_devices:
LOG.warn(
"Device list appears to be desynchronized, forcing a refresh thru the API..."
)
await self.devices.update()

updated_devices = self.devices.parse_values_received(event.arguments[0])
Expand All @@ -270,21 +274,38 @@ async def on_websocket_event(self, event: WebsocketEvent) -> None:
self._hass, SIGNAL_UPDATE_ENTITY.format(device.id)
)
elif event.target == "DeviceListInitialValuesReceived":
# This websocket event only happens on initial connection
# This websocket event only happens after calling SubscribeToLocation.
# This triggers an update without throwing an exception
await self.devices.update()
new_devices = await self.devices.update_devicelist_from_signalr(
event.arguments[0]
)
elif event.target == "DeviceListUpdatedValuesReceived":
# This message only contains display informations, such as the Device's name (as set in the app), it's groupid, icon, etc.
# Updating the device name causes issues in the integration, it detects it as a new device and creates a new entity.
# Ignore this call, for now... (update_devicelist_from_signalr does work, but causes the issue above)
# await self.devices.update_devicelist_from_signalr(event.arguments[0])
LOG.debug(
"Received 'DeviceListUpdatedValuesReceived' message, not implemented yet."
)
elif event.target == "DevicesListChanged":
# DeviceListChanged only triggers when unpairing devices
# Forcing an update when that happens, even though pyhilo doesn't
# manage device removal currently.
await self.devices.update()
# This message only contains the location_id and is used to inform us that devices have been removed from the location.
# Device deletion is not implemented yet, so we just log the message for now.
LOG.debug("Received 'DevicesListChanged' message, not implemented yet.")
elif event.target == "DeviceAdded":
# Same structure as DeviceList* but only one device instead of a list
devices = []
devices.append(event.arguments[0])
new_devices = await self.devices.update_devicelist_from_signalr(devices)
elif event.target == "DeviceDeleted":
# Device deletion is not implemented yet, so we just log the message for now.
LOG.debug("Received 'DeviceDeleted' message, not implemented yet.")
elif event.target == "GatewayValuesReceived":
# Gateway deviceId hardcoded to 1 as it is not returned by Gateways/Info.
# First time we encounter a GatewayValueReceived event, update device with proper deviceid.
gateway = self.devices.find_device(1)
if gateway is not None:
if gateway:
gateway.id = event.arguments[0][0]["deviceId"]
LOG.debug("Updated Gateway's deviceId from default 1 to {gateway.id}")
LOG.debug(f"Updated Gateway's deviceId from default 1 to {gateway.id}")

updated_devices = self.devices.parse_values_received(event.arguments[0])
# NOTE(dvd): If we don't do this, we need to wait until the coordinator
Expand All @@ -304,14 +325,6 @@ async def subscribe_to_location(self, inv_id: int) -> None:
[self.devices.location_id], "SubscribeToLocation", inv_id
)

@callback
async def subscribe_to_attributes(self, inv_id: int) -> None:
"""Sends the json payload to receive the device attributes."""
LOG.debug(f"Subscribing to attributes {self.devices.attributes_list}")
await self._api.websocket.async_invoke(
self.devices.attributes_list, "SubscribeDevicesAttributes", inv_id
)

@callback
async def request_status_update(self) -> None:
await self._api.websocket.send_status()
Expand Down Expand Up @@ -613,7 +626,7 @@ def __init__(

@property
def should_poll(self) -> bool:
return False if self._device.type != "Gateway" else True
return False

@property
def available(self) -> bool:
Expand Down
7 changes: 4 additions & 3 deletions custom_components/hilo/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
DEFAULT_SCAN_INTERVAL = 300
EVENT_SCAN_INTERVAL = 3000
REWARD_SCAN_INTERVAL = 7200
MIN_SCAN_INTERVAL = 15
NOTIFICATION_SCAN_INTERVAL = 1800
MIN_SCAN_INTERVAL = 60

CONF_TARIFF = {
"rate d": {
Expand Down Expand Up @@ -71,5 +72,5 @@
"OutdoorWeatherStation",
"Gateway",
]
CLIMATE_CLASSES = ["Thermostat"]
SWITCH_CLASSES = ["Outlet"]
CLIMATE_CLASSES = ["Thermostat", "FloorThermostat", "Thermostat24V"]
SWITCH_CLASSES = ["Outlet", "Ccr", "Cee"]
54 changes: 47 additions & 7 deletions custom_components/hilo/sensor.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Support for various Hilo sensors."""
from __future__ import annotations

from datetime import timedelta
from datetime import datetime, timedelta, timezone

from homeassistant.components.integration.sensor import METHOD_LEFT, IntegrationSensor
from homeassistant.components.sensor import (
Expand Down Expand Up @@ -47,6 +47,7 @@
HILO_ENERGY_TOTAL,
HILO_SENSOR_CLASSES,
LOG,
NOTIFICATION_SCAN_INTERVAL,
REWARD_SCAN_INTERVAL,
TARIFF_LIST,
)
Expand Down Expand Up @@ -252,7 +253,7 @@ def __init__(self, device):
self._attr_name = HILO_ENERGY_TOTAL
self._unit_of_measurement = ENERGY_KILO_WATT_HOUR
self._unit_prefix = "k"
if device.type == "Thermostat":
if device.type == "Thermostat" or device.type == "FloorThermostat":
self._unit_of_measurement = ENERGY_KILO_WATT_HOUR
self._unit_prefix = "k"
self._source = f"sensor.{slugify(device.name)}_power"
Expand Down Expand Up @@ -428,14 +429,16 @@ def extra_state_attributes(self):
class HiloNotificationSensor(HiloEntity, RestoreEntity, SensorEntity):
"""Hilo Notification sensor.
Its state will be the number of notification waiting in the Hilo app.
Notifications only used for OneLink's alerts & Low-battery warnings.
We should consider having this sensor enabled only if a smoke detector is in use.
"""

def __init__(self, hilo, device, scan_interval):
self._attr_name = "Notifications Hilo"
super().__init__(hilo, name=self._attr_name, device=device)
self._attr_unique_id = slugify(self._attr_name)
LOG.debug(f"Setting up NotificationSensor entity: {self._attr_name}")
self.scan_interval = timedelta(seconds=scan_interval)
self.scan_interval = timedelta(seconds=NOTIFICATION_SCAN_INTERVAL)
self._state = 0
self._notifications = []
self.async_update = Throttle(self.scan_interval)(self._async_update)
Expand Down Expand Up @@ -539,16 +542,53 @@ async def async_added_to_hass(self):
async def _async_update(self):
seasons = await self._hilo._api.get_seasons(self._hilo.devices.location_id)
if seasons:
current_history = self._history
new_history = []

for idx, season in enumerate(seasons):
current_history_season = next(
(
item
for item in current_history
if item.get("season") == season.get("season")
),
None,
)

if idx == 0:
self._state = season.get("totalReward", 0)
events = []
for raw_event in season.get("events", []):
details = await self._hilo._api.get_gd_events(
self._hilo.devices.location_id, event_id=raw_event["id"]
)
events.append(Event(**details).as_dict())
current_history_event = None
event = None

if current_history_season:
current_history_event = next(
(
ev
for ev in current_history_season["events"]
if ev["event_id"] == raw_event["id"]
),
None,
)

start_date_utc = datetime.fromisoformat(raw_event["startDateUtc"])
event_age = datetime.now(timezone.utc) - start_date_utc
if (
current_history_event
and current_history_event.get("state") == "completed"
and event_age > timedelta(days=1)
):
# No point updating events for previously completed events, they won't change.
event = current_history_event
else:
# Either it is an unknown event, one that is still in progress or a recent one, get the details.
details = await self._hilo._api.get_gd_events(
self._hilo.devices.location_id, event_id=raw_event["id"]
)
event = Event(**details).as_dict()

events.append(event)
season["events"] = events
new_history.append(season)
self._history = new_history
Expand Down

0 comments on commit 23c20b0

Please sign in to comment.