From e344c2ea6405d563899f70b2b98f7125b8f0ce34 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2020 18:28:55 -0500 Subject: [PATCH] Add gate support to myq, fix bouncy updates (#33124) * Add gate support to myq, fix bouncy updates Switch to DataUpdateCoordinator, previously we would hit the myq api every 60 seconds per device. If you have access to 20 garage doors on the account it means we would have previously tried to update 20 times per minutes. * switch to async_call_later --- homeassistant/components/myq/__init__.py | 14 +++- homeassistant/components/myq/const.py | 31 +++++++- homeassistant/components/myq/cover.py | 94 +++++++++++++++++++++--- 3 files changed, 124 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py index 51ad9fb48f0..fc1d374fe43 100644 --- a/homeassistant/components/myq/__init__.py +++ b/homeassistant/components/myq/__init__.py @@ -1,5 +1,6 @@ """The MyQ integration.""" import asyncio +from datetime import timedelta import logging import pymyq @@ -10,8 +11,9 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, PLATFORMS +from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, PLATFORMS, UPDATE_INTERVAL _LOGGER = logging.getLogger(__name__) @@ -38,7 +40,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except MyQError: raise ConfigEntryNotReady - hass.data[DOMAIN][entry.entry_id] = myq + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="myq devices", + update_method=myq.update_device_info, + update_interval=timedelta(seconds=UPDATE_INTERVAL), + ) + + hass.data[DOMAIN][entry.entry_id] = {MYQ_GATEWAY: myq, MYQ_COORDINATOR: coordinator} for component in PLATFORMS: hass.async_create_task( diff --git a/homeassistant/components/myq/const.py b/homeassistant/components/myq/const.py index 260811e54ce..dcae53bd080 100644 --- a/homeassistant/components/myq/const.py +++ b/homeassistant/components/myq/const.py @@ -1,4 +1,11 @@ """The MyQ integration.""" +from pymyq.device import ( + STATE_CLOSED as MYQ_STATE_CLOSED, + STATE_CLOSING as MYQ_STATE_CLOSING, + STATE_OPEN as MYQ_STATE_OPEN, + STATE_OPENING as MYQ_STATE_OPENING, +) + from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING DOMAIN = "myq" @@ -10,9 +17,25 @@ MYQ_DEVICE_TYPE_GATE = "gate" MYQ_DEVICE_STATE = "state" MYQ_DEVICE_STATE_ONLINE = "online" + MYQ_TO_HASS = { - "closed": STATE_CLOSED, - "closing": STATE_CLOSING, - "open": STATE_OPEN, - "opening": STATE_OPENING, + MYQ_STATE_CLOSED: STATE_CLOSED, + MYQ_STATE_CLOSING: STATE_CLOSING, + MYQ_STATE_OPEN: STATE_OPEN, + MYQ_STATE_OPENING: STATE_OPENING, } + +MYQ_GATEWAY = "myq_gateway" +MYQ_COORDINATOR = "coordinator" + +# myq has some ratelimits in place +# and 61 seemed to be work every time +UPDATE_INTERVAL = 61 + +# Estimated time it takes myq to start transition from one +# state to the next. +TRANSITION_START_DURATION = 7 + +# Estimated time it takes myq to complete a transition +# from one state to another +TRANSITION_COMPLETE_DURATION = 37 diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index 0df61b4d5db..21eca6179dd 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -1,9 +1,12 @@ """Support for MyQ-Enabled Garage Doors.""" import logging +import time import voluptuous as vol from homeassistant.components.cover import ( + DEVICE_CLASS_GARAGE, + DEVICE_CLASS_GATE, PLATFORM_SCHEMA, SUPPORT_CLOSE, SUPPORT_OPEN, @@ -18,9 +21,22 @@ from homeassistant.const import ( STATE_CLOSING, STATE_OPENING, ) +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.event import async_call_later -from .const import DOMAIN, MYQ_DEVICE_STATE, MYQ_DEVICE_STATE_ONLINE, MYQ_TO_HASS +from .const import ( + DOMAIN, + MYQ_COORDINATOR, + MYQ_DEVICE_STATE, + MYQ_DEVICE_STATE_ONLINE, + MYQ_DEVICE_TYPE, + MYQ_DEVICE_TYPE_GATE, + MYQ_GATEWAY, + MYQ_TO_HASS, + TRANSITION_COMPLETE_DURATION, + TRANSITION_START_DURATION, +) _LOGGER = logging.getLogger(__name__) @@ -53,21 +69,32 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up mysq covers.""" - myq = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities([MyQDevice(device) for device in myq.covers.values()], True) + data = hass.data[DOMAIN][config_entry.entry_id] + myq = data[MYQ_GATEWAY] + coordinator = data[MYQ_COORDINATOR] + + async_add_entities( + [MyQDevice(coordinator, device) for device in myq.covers.values()], True + ) class MyQDevice(CoverDevice): """Representation of a MyQ cover.""" - def __init__(self, device): + def __init__(self, coordinator, device): """Initialize with API object, device id.""" + self._coordinator = coordinator self._device = device + self._last_action_timestamp = 0 + self._scheduled_transition_update = None @property def device_class(self): """Define this cover as a garage door.""" - return "garage" + device_type = self._device.device_json.get(MYQ_DEVICE_TYPE) + if device_type is not None and device_type == MYQ_DEVICE_TYPE_GATE: + return DEVICE_CLASS_GATE + return DEVICE_CLASS_GARAGE @property def name(self): @@ -77,6 +104,9 @@ class MyQDevice(CoverDevice): @property def available(self): """Return if the device is online.""" + if not self._coordinator.last_update_success: + return False + # Not all devices report online so assume True if its missing return self._device.device_json[MYQ_DEVICE_STATE].get( MYQ_DEVICE_STATE_ONLINE, True @@ -109,19 +139,41 @@ class MyQDevice(CoverDevice): async def async_close_cover(self, **kwargs): """Issue close command to cover.""" + self._last_action_timestamp = time.time() await self._device.close() - # Writes closing state - self.async_write_ha_state() + self._async_schedule_update_for_transition() async def async_open_cover(self, **kwargs): """Issue open command to cover.""" + self._last_action_timestamp = time.time() await self._device.open() - # Writes opening state + self._async_schedule_update_for_transition() + + @callback + def _async_schedule_update_for_transition(self): self.async_write_ha_state() + # Cancel any previous updates + if self._scheduled_transition_update: + self._scheduled_transition_update() + + # Schedule an update for when we expect the transition + # to be completed so the garage door or gate does not + # seem like its closing or opening for a long time + self._scheduled_transition_update = async_call_later( + self.hass, + TRANSITION_COMPLETE_DURATION, + self._async_complete_schedule_update, + ) + + async def _async_complete_schedule_update(self, _): + """Update status of the cover via coordinator.""" + self._scheduled_transition_update = None + await self._coordinator.async_request_refresh() + async def async_update(self): """Update status of cover.""" - await self._device.update() + await self._coordinator.async_request_refresh() @property def device_info(self): @@ -135,3 +187,27 @@ class MyQDevice(CoverDevice): if self._device.parent_device_id: device_info["via_device"] = (DOMAIN, self._device.parent_device_id) return device_info + + @callback + def _async_consume_update(self): + if time.time() - self._last_action_timestamp <= TRANSITION_START_DURATION: + # If we just started a transition we need + # to prevent a bouncy state + return + + self.async_write_ha_state() + + @property + def should_poll(self): + """Return False, updates are controlled via coordinator.""" + return False + + async def async_added_to_hass(self): + """Subscribe to updates.""" + self._coordinator.async_add_listener(self._async_consume_update) + + async def async_will_remove_from_hass(self): + """Undo subscription.""" + self._coordinator.async_remove_listener(self._async_consume_update) + if self._scheduled_transition_update: + self._scheduled_transition_update()