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_laterpull/33173/head
parent
ab8c50895e
commit
e344c2ea64
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue