mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
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:
parent
ab8c50895e
commit
e344c2ea64
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user