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
This commit is contained in:
J. Nick Koston 2020-03-22 18:28:55 -05:00 committed by GitHub
parent ab8c50895e
commit e344c2ea64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 124 additions and 15 deletions

View File

@ -1,5 +1,6 @@
"""The MyQ integration.""" """The MyQ integration."""
import asyncio import asyncio
from datetime import timedelta
import logging import logging
import pymyq import pymyq
@ -10,8 +11,9 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client 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__) _LOGGER = logging.getLogger(__name__)
@ -38,7 +40,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
except MyQError: except MyQError:
raise ConfigEntryNotReady 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: for component in PLATFORMS:
hass.async_create_task( hass.async_create_task(

View File

@ -1,4 +1,11 @@
"""The MyQ integration.""" """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 from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING
DOMAIN = "myq" DOMAIN = "myq"
@ -10,9 +17,25 @@ MYQ_DEVICE_TYPE_GATE = "gate"
MYQ_DEVICE_STATE = "state" MYQ_DEVICE_STATE = "state"
MYQ_DEVICE_STATE_ONLINE = "online" MYQ_DEVICE_STATE_ONLINE = "online"
MYQ_TO_HASS = { MYQ_TO_HASS = {
"closed": STATE_CLOSED, MYQ_STATE_CLOSED: STATE_CLOSED,
"closing": STATE_CLOSING, MYQ_STATE_CLOSING: STATE_CLOSING,
"open": STATE_OPEN, MYQ_STATE_OPEN: STATE_OPEN,
"opening": STATE_OPENING, 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

View File

@ -1,9 +1,12 @@
"""Support for MyQ-Enabled Garage Doors.""" """Support for MyQ-Enabled Garage Doors."""
import logging import logging
import time
import voluptuous as vol import voluptuous as vol
from homeassistant.components.cover import ( from homeassistant.components.cover import (
DEVICE_CLASS_GARAGE,
DEVICE_CLASS_GATE,
PLATFORM_SCHEMA, PLATFORM_SCHEMA,
SUPPORT_CLOSE, SUPPORT_CLOSE,
SUPPORT_OPEN, SUPPORT_OPEN,
@ -18,9 +21,22 @@ from homeassistant.const import (
STATE_CLOSING, STATE_CLOSING,
STATE_OPENING, STATE_OPENING,
) )
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv 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__) _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): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up mysq covers.""" """Set up mysq covers."""
myq = hass.data[DOMAIN][config_entry.entry_id] data = hass.data[DOMAIN][config_entry.entry_id]
async_add_entities([MyQDevice(device) for device in myq.covers.values()], True) myq = data[MYQ_GATEWAY]
coordinator = data[MYQ_COORDINATOR]
async_add_entities(
[MyQDevice(coordinator, device) for device in myq.covers.values()], True
)
class MyQDevice(CoverDevice): class MyQDevice(CoverDevice):
"""Representation of a MyQ cover.""" """Representation of a MyQ cover."""
def __init__(self, device): def __init__(self, coordinator, device):
"""Initialize with API object, device id.""" """Initialize with API object, device id."""
self._coordinator = coordinator
self._device = device self._device = device
self._last_action_timestamp = 0
self._scheduled_transition_update = None
@property @property
def device_class(self): def device_class(self):
"""Define this cover as a garage door.""" """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 @property
def name(self): def name(self):
@ -77,6 +104,9 @@ class MyQDevice(CoverDevice):
@property @property
def available(self): def available(self):
"""Return if the device is online.""" """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 # Not all devices report online so assume True if its missing
return self._device.device_json[MYQ_DEVICE_STATE].get( return self._device.device_json[MYQ_DEVICE_STATE].get(
MYQ_DEVICE_STATE_ONLINE, True MYQ_DEVICE_STATE_ONLINE, True
@ -109,19 +139,41 @@ class MyQDevice(CoverDevice):
async def async_close_cover(self, **kwargs): async def async_close_cover(self, **kwargs):
"""Issue close command to cover.""" """Issue close command to cover."""
self._last_action_timestamp = time.time()
await self._device.close() await self._device.close()
# Writes closing state self._async_schedule_update_for_transition()
self.async_write_ha_state()
async def async_open_cover(self, **kwargs): async def async_open_cover(self, **kwargs):
"""Issue open command to cover.""" """Issue open command to cover."""
self._last_action_timestamp = time.time()
await self._device.open() 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() 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): async def async_update(self):
"""Update status of cover.""" """Update status of cover."""
await self._device.update() await self._coordinator.async_request_refresh()
@property @property
def device_info(self): def device_info(self):
@ -135,3 +187,27 @@ class MyQDevice(CoverDevice):
if self._device.parent_device_id: if self._device.parent_device_id:
device_info["via_device"] = (DOMAIN, self._device.parent_device_id) device_info["via_device"] = (DOMAIN, self._device.parent_device_id)
return device_info 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()