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."""
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(

View File

@ -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

View File

@ -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()