diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py index 51ad9fb48f060914ec40a7cc38b0c9136b7943e6..fc1d374fe43cd3f917d83574b73728ccc8ed0f4d 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 260811e54ce5099fbc9f1c866a96460e18002475..dcae53bd08023a1d8dfce2d74d8ace5ee39ca086 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 0df61b4d5dbe86bcfbe9a80f8a42ac870a155e9d..21eca6179dd4ba27c7557c31db7a76992dc8df0a 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 .const import DOMAIN, MYQ_DEVICE_STATE, MYQ_DEVICE_STATE_ONLINE, MYQ_TO_HASS +from homeassistant.helpers.event import async_call_later + +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()