Skip to content

Commit e344c2e

Browse files
authored
Add gate support to myq, fix bouncy updates (home-assistant#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
1 parent ab8c508 commit e344c2e

File tree

3 files changed

+125
-16
lines changed

3 files changed

+125
-16
lines changed

homeassistant/components/myq/__init__.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""The MyQ integration."""
22
import asyncio
3+
from datetime import timedelta
34
import logging
45

56
import pymyq
@@ -10,8 +11,9 @@
1011
from homeassistant.core import HomeAssistant
1112
from homeassistant.exceptions import ConfigEntryNotReady
1213
from homeassistant.helpers import aiohttp_client
14+
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
1315

14-
from .const import DOMAIN, PLATFORMS
16+
from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, PLATFORMS, UPDATE_INTERVAL
1517

1618
_LOGGER = logging.getLogger(__name__)
1719

@@ -38,7 +40,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
3840
except MyQError:
3941
raise ConfigEntryNotReady
4042

41-
hass.data[DOMAIN][entry.entry_id] = myq
43+
coordinator = DataUpdateCoordinator(
44+
hass,
45+
_LOGGER,
46+
name="myq devices",
47+
update_method=myq.update_device_info,
48+
update_interval=timedelta(seconds=UPDATE_INTERVAL),
49+
)
50+
51+
hass.data[DOMAIN][entry.entry_id] = {MYQ_GATEWAY: myq, MYQ_COORDINATOR: coordinator}
4252

4353
for component in PLATFORMS:
4454
hass.async_create_task(

homeassistant/components/myq/const.py

+27-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
"""The MyQ integration."""
2+
from pymyq.device import (
3+
STATE_CLOSED as MYQ_STATE_CLOSED,
4+
STATE_CLOSING as MYQ_STATE_CLOSING,
5+
STATE_OPEN as MYQ_STATE_OPEN,
6+
STATE_OPENING as MYQ_STATE_OPENING,
7+
)
8+
29
from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING
310

411
DOMAIN = "myq"
@@ -10,9 +17,25 @@
1017
MYQ_DEVICE_STATE = "state"
1118
MYQ_DEVICE_STATE_ONLINE = "online"
1219

20+
1321
MYQ_TO_HASS = {
14-
"closed": STATE_CLOSED,
15-
"closing": STATE_CLOSING,
16-
"open": STATE_OPEN,
17-
"opening": STATE_OPENING,
22+
MYQ_STATE_CLOSED: STATE_CLOSED,
23+
MYQ_STATE_CLOSING: STATE_CLOSING,
24+
MYQ_STATE_OPEN: STATE_OPEN,
25+
MYQ_STATE_OPENING: STATE_OPENING,
1826
}
27+
28+
MYQ_GATEWAY = "myq_gateway"
29+
MYQ_COORDINATOR = "coordinator"
30+
31+
# myq has some ratelimits in place
32+
# and 61 seemed to be work every time
33+
UPDATE_INTERVAL = 61
34+
35+
# Estimated time it takes myq to start transition from one
36+
# state to the next.
37+
TRANSITION_START_DURATION = 7
38+
39+
# Estimated time it takes myq to complete a transition
40+
# from one state to another
41+
TRANSITION_COMPLETE_DURATION = 37

homeassistant/components/myq/cover.py

+86-10
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
"""Support for MyQ-Enabled Garage Doors."""
22
import logging
3+
import time
34

45
import voluptuous as vol
56

67
from homeassistant.components.cover import (
8+
DEVICE_CLASS_GARAGE,
9+
DEVICE_CLASS_GATE,
710
PLATFORM_SCHEMA,
811
SUPPORT_CLOSE,
912
SUPPORT_OPEN,
@@ -18,9 +21,22 @@
1821
STATE_CLOSING,
1922
STATE_OPENING,
2023
)
24+
from homeassistant.core import callback
2125
from homeassistant.helpers import config_validation as cv
22-
23-
from .const import DOMAIN, MYQ_DEVICE_STATE, MYQ_DEVICE_STATE_ONLINE, MYQ_TO_HASS
26+
from homeassistant.helpers.event import async_call_later
27+
28+
from .const import (
29+
DOMAIN,
30+
MYQ_COORDINATOR,
31+
MYQ_DEVICE_STATE,
32+
MYQ_DEVICE_STATE_ONLINE,
33+
MYQ_DEVICE_TYPE,
34+
MYQ_DEVICE_TYPE_GATE,
35+
MYQ_GATEWAY,
36+
MYQ_TO_HASS,
37+
TRANSITION_COMPLETE_DURATION,
38+
TRANSITION_START_DURATION,
39+
)
2440

2541
_LOGGER = logging.getLogger(__name__)
2642

@@ -53,21 +69,32 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
5369

5470
async def async_setup_entry(hass, config_entry, async_add_entities):
5571
"""Set up mysq covers."""
56-
myq = hass.data[DOMAIN][config_entry.entry_id]
57-
async_add_entities([MyQDevice(device) for device in myq.covers.values()], True)
72+
data = hass.data[DOMAIN][config_entry.entry_id]
73+
myq = data[MYQ_GATEWAY]
74+
coordinator = data[MYQ_COORDINATOR]
75+
76+
async_add_entities(
77+
[MyQDevice(coordinator, device) for device in myq.covers.values()], True
78+
)
5879

5980

6081
class MyQDevice(CoverDevice):
6182
"""Representation of a MyQ cover."""
6283

63-
def __init__(self, device):
84+
def __init__(self, coordinator, device):
6485
"""Initialize with API object, device id."""
86+
self._coordinator = coordinator
6587
self._device = device
88+
self._last_action_timestamp = 0
89+
self._scheduled_transition_update = None
6690

6791
@property
6892
def device_class(self):
6993
"""Define this cover as a garage door."""
70-
return "garage"
94+
device_type = self._device.device_json.get(MYQ_DEVICE_TYPE)
95+
if device_type is not None and device_type == MYQ_DEVICE_TYPE_GATE:
96+
return DEVICE_CLASS_GATE
97+
return DEVICE_CLASS_GARAGE
7198

7299
@property
73100
def name(self):
@@ -77,6 +104,9 @@ def name(self):
77104
@property
78105
def available(self):
79106
"""Return if the device is online."""
107+
if not self._coordinator.last_update_success:
108+
return False
109+
80110
# Not all devices report online so assume True if its missing
81111
return self._device.device_json[MYQ_DEVICE_STATE].get(
82112
MYQ_DEVICE_STATE_ONLINE, True
@@ -109,19 +139,41 @@ def unique_id(self):
109139

110140
async def async_close_cover(self, **kwargs):
111141
"""Issue close command to cover."""
142+
self._last_action_timestamp = time.time()
112143
await self._device.close()
113-
# Writes closing state
114-
self.async_write_ha_state()
144+
self._async_schedule_update_for_transition()
115145

116146
async def async_open_cover(self, **kwargs):
117147
"""Issue open command to cover."""
148+
self._last_action_timestamp = time.time()
118149
await self._device.open()
119-
# Writes opening state
150+
self._async_schedule_update_for_transition()
151+
152+
@callback
153+
def _async_schedule_update_for_transition(self):
120154
self.async_write_ha_state()
121155

156+
# Cancel any previous updates
157+
if self._scheduled_transition_update:
158+
self._scheduled_transition_update()
159+
160+
# Schedule an update for when we expect the transition
161+
# to be completed so the garage door or gate does not
162+
# seem like its closing or opening for a long time
163+
self._scheduled_transition_update = async_call_later(
164+
self.hass,
165+
TRANSITION_COMPLETE_DURATION,
166+
self._async_complete_schedule_update,
167+
)
168+
169+
async def _async_complete_schedule_update(self, _):
170+
"""Update status of the cover via coordinator."""
171+
self._scheduled_transition_update = None
172+
await self._coordinator.async_request_refresh()
173+
122174
async def async_update(self):
123175
"""Update status of cover."""
124-
await self._device.update()
176+
await self._coordinator.async_request_refresh()
125177

126178
@property
127179
def device_info(self):
@@ -135,3 +187,27 @@ def device_info(self):
135187
if self._device.parent_device_id:
136188
device_info["via_device"] = (DOMAIN, self._device.parent_device_id)
137189
return device_info
190+
191+
@callback
192+
def _async_consume_update(self):
193+
if time.time() - self._last_action_timestamp <= TRANSITION_START_DURATION:
194+
# If we just started a transition we need
195+
# to prevent a bouncy state
196+
return
197+
198+
self.async_write_ha_state()
199+
200+
@property
201+
def should_poll(self):
202+
"""Return False, updates are controlled via coordinator."""
203+
return False
204+
205+
async def async_added_to_hass(self):
206+
"""Subscribe to updates."""
207+
self._coordinator.async_add_listener(self._async_consume_update)
208+
209+
async def async_will_remove_from_hass(self):
210+
"""Undo subscription."""
211+
self._coordinator.async_remove_listener(self._async_consume_update)
212+
if self._scheduled_transition_update:
213+
self._scheduled_transition_update()

0 commit comments

Comments
 (0)