diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py index 959000da3b3..6b3a52ba7b0 100644 --- a/homeassistant/components/myq/__init__.py +++ b/homeassistant/components/myq/__init__.py @@ -11,7 +11,7 @@ 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 homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, PLATFORMS, UPDATE_INTERVAL @@ -40,11 +40,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except MyQError as err: raise ConfigEntryNotReady from err + # Called by DataUpdateCoordinator, allows to capture any MyQError exceptions and to throw an HASS UpdateFailed + # exception instead, preventing traceback in HASS logs. + async def async_update_data(): + try: + return await myq.update_device_info() + except MyQError as err: + raise UpdateFailed(str(err)) from err + coordinator = DataUpdateCoordinator( hass, _LOGGER, name="myq devices", - update_method=myq.update_device_info, + update_method=async_update_data, update_interval=timedelta(seconds=UPDATE_INTERVAL), ) diff --git a/homeassistant/components/myq/binary_sensor.py b/homeassistant/components/myq/binary_sensor.py index 57bd2451d2a..e3832458b9b 100644 --- a/homeassistant/components/myq/binary_sensor.py +++ b/homeassistant/components/myq/binary_sensor.py @@ -1,7 +1,5 @@ """Support for MyQ gateways.""" from pymyq.const import ( - DEVICE_FAMILY as MYQ_DEVICE_FAMILY, - DEVICE_FAMILY_GATEWAY as MYQ_DEVICE_FAMILY_GATEWAY, DEVICE_STATE as MYQ_DEVICE_STATE, DEVICE_STATE_ONLINE as MYQ_DEVICE_STATE_ONLINE, KNOWN_MODELS, @@ -25,9 +23,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] - for device in myq.devices.values(): - if device.device_json[MYQ_DEVICE_FAMILY] == MYQ_DEVICE_FAMILY_GATEWAY: - entities.append(MyQBinarySensorEntity(coordinator, device)) + for device in myq.gateways.values(): + entities.append(MyQBinarySensorEntity(coordinator, device)) async_add_entities(entities, True) diff --git a/homeassistant/components/myq/const.py b/homeassistant/components/myq/const.py index 9251bce7447..6189b1601ea 100644 --- a/homeassistant/components/myq/const.py +++ b/homeassistant/components/myq/const.py @@ -1,9 +1,9 @@ """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 pymyq.garagedoor import ( + STATE_CLOSED as MYQ_COVER_STATE_CLOSED, + STATE_CLOSING as MYQ_COVER_STATE_CLOSING, + STATE_OPEN as MYQ_COVER_STATE_OPEN, + STATE_OPENING as MYQ_COVER_STATE_OPENING, ) from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING @@ -13,10 +13,10 @@ DOMAIN = "myq" PLATFORMS = ["cover", "binary_sensor"] MYQ_TO_HASS = { - MYQ_STATE_CLOSED: STATE_CLOSED, - MYQ_STATE_CLOSING: STATE_CLOSING, - MYQ_STATE_OPEN: STATE_OPEN, - MYQ_STATE_OPENING: STATE_OPENING, + MYQ_COVER_STATE_CLOSED: STATE_CLOSED, + MYQ_COVER_STATE_CLOSING: STATE_CLOSING, + MYQ_COVER_STATE_OPEN: STATE_OPEN, + MYQ_COVER_STATE_OPENING: STATE_OPENING, } MYQ_GATEWAY = "myq_gateway" @@ -24,7 +24,7 @@ MYQ_COORDINATOR = "coordinator" # myq has some ratelimits in place # and 61 seemed to be work every time -UPDATE_INTERVAL = 61 +UPDATE_INTERVAL = 15 # Estimated time it takes myq to start transition from one # state to the next. diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index 6fef6b25bab..e26a969e724 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -1,14 +1,14 @@ """Support for MyQ-Enabled Garage Doors.""" -import time +import logging from pymyq.const import ( DEVICE_STATE as MYQ_DEVICE_STATE, DEVICE_STATE_ONLINE as MYQ_DEVICE_STATE_ONLINE, - DEVICE_TYPE as MYQ_DEVICE_TYPE, DEVICE_TYPE_GATE as MYQ_DEVICE_TYPE_GATE, KNOWN_MODELS, MANUFACTURER, ) +from pymyq.errors import MyQError from homeassistant.components.cover import ( DEVICE_CLASS_GARAGE, @@ -17,19 +17,12 @@ from homeassistant.components.cover import ( SUPPORT_OPEN, CoverEntity, ) -from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPENING -from homeassistant.core import callback -from homeassistant.helpers.event import async_call_later +from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ( - DOMAIN, - MYQ_COORDINATOR, - MYQ_GATEWAY, - MYQ_TO_HASS, - TRANSITION_COMPLETE_DURATION, - TRANSITION_START_DURATION, -) +from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, MYQ_TO_HASS + +_LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -50,13 +43,11 @@ class MyQDevice(CoordinatorEntity, CoverEntity): """Initialize with API object, device id.""" super().__init__(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.""" - device_type = self._device.device_json.get(MYQ_DEVICE_TYPE) + device_type = self._device.device_type if device_type is not None and device_type == MYQ_DEVICE_TYPE_GATE: return DEVICE_CLASS_GATE return DEVICE_CLASS_GARAGE @@ -87,6 +78,11 @@ class MyQDevice(CoordinatorEntity, CoverEntity): """Return if the cover is closing or not.""" return MYQ_TO_HASS.get(self._device.state) == STATE_CLOSING + @property + def is_open(self): + """Return if the cover is opening or not.""" + return MYQ_TO_HASS.get(self._device.state) == STATE_OPEN + @property def is_opening(self): """Return if the cover is opening or not.""" @@ -104,37 +100,48 @@ class MyQDevice(CoordinatorEntity, CoverEntity): async def async_close_cover(self, **kwargs): """Issue close command to cover.""" - self._last_action_timestamp = time.time() - await self._device.close() - self._async_schedule_update_for_transition() + if self.is_closing or self.is_closed: + return + + try: + wait_task = await self._device.close(wait_for_state=False) + except MyQError as err: + _LOGGER.error( + "Closing of cover %s failed with error: %s", self._device.name, str(err) + ) + + return + + # Write closing state to HASS + self.async_write_ha_state() + + if not await wait_task: + _LOGGER.error("Closing of cover %s failed", self._device.name) + + # Write final state to HASS + self.async_write_ha_state() async def async_open_cover(self, **kwargs): """Issue open command to cover.""" - self._last_action_timestamp = time.time() - await self._device.open() - self._async_schedule_update_for_transition() + if self.is_opening or self.is_open: + return - @callback - def _async_schedule_update_for_transition(self): + try: + wait_task = await self._device.open(wait_for_state=False) + except MyQError as err: + _LOGGER.error( + "Opening of cover %s failed with error: %s", self._device.name, str(err) + ) + return + + # Write opening state to HASS self.async_write_ha_state() - # Cancel any previous updates - if self._scheduled_transition_update: - self._scheduled_transition_update() + if not await wait_task: + _LOGGER.error("Opening of cover %s failed", self._device.name) - # 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() + # Write final state to HASS + self.async_write_ha_state() @property def device_info(self): @@ -152,22 +159,8 @@ class MyQDevice(CoordinatorEntity, CoverEntity): 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() - async def async_added_to_hass(self): """Subscribe to updates.""" self.async_on_remove( - self.coordinator.async_add_listener(self._async_consume_update) + self.coordinator.async_add_listener(self.async_write_ha_state) ) - - async def async_will_remove_from_hass(self): - """Undo subscription.""" - if self._scheduled_transition_update: - self._scheduled_transition_update() diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index aba2f24b5bd..9dc8719ed4e 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -2,7 +2,7 @@ "domain": "myq", "name": "MyQ", "documentation": "https://www.home-assistant.io/integrations/myq", - "requirements": ["pymyq==2.0.14"], + "requirements": ["pymyq==3.0.1"], "codeowners": ["@bdraco"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index ee86d62270a..12ade1a446b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1545,7 +1545,7 @@ pymsteams==0.1.12 pymusiccast==0.1.6 # homeassistant.components.myq -pymyq==2.0.14 +pymyq==3.0.1 # homeassistant.components.mysensors pymysensors==0.20.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d63ba30e1c..f5a629a771f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -808,7 +808,7 @@ pymodbus==2.3.0 pymonoprice==0.3 # homeassistant.components.myq -pymyq==2.0.14 +pymyq==3.0.1 # homeassistant.components.mysensors pymysensors==0.20.1 diff --git a/tests/components/myq/util.py b/tests/components/myq/util.py index 84e85723918..8cb0d17f592 100644 --- a/tests/components/myq/util.py +++ b/tests/components/myq/util.py @@ -1,14 +1,18 @@ """Tests for the myq integration.""" - import json +import logging from unittest.mock import patch +from pymyq.const import ACCOUNTS_ENDPOINT, DEVICES_ENDPOINT + from homeassistant.components.myq.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture +_LOGGER = logging.getLogger(__name__) + async def async_init_integration( hass: HomeAssistant, @@ -20,16 +24,24 @@ async def async_init_integration( devices_json = load_fixture(devices_fixture) devices_dict = json.loads(devices_json) - def _handle_mock_api_request(method, endpoint, **kwargs): - if endpoint == "Login": - return {"SecurityToken": 1234} - if endpoint == "My": - return {"Account": {"Id": 1}} - if endpoint == "Accounts/1/Devices": - return devices_dict - return {} + def _handle_mock_api_oauth_authenticate(): + return 1234, 1800 - with patch("pymyq.api.API.request", side_effect=_handle_mock_api_request): + def _handle_mock_api_request(method, returns, url, **kwargs): + _LOGGER.debug("URL: %s", url) + if url == ACCOUNTS_ENDPOINT: + _LOGGER.debug("Accounts") + return None, {"accounts": [{"id": 1, "name": "mock"}]} + if url == DEVICES_ENDPOINT.format(account_id=1): + _LOGGER.debug("Devices") + return None, devices_dict + _LOGGER.debug("Something else") + return None, {} + + with patch( + "pymyq.api.API._oauth_authenticate", + side_effect=_handle_mock_api_oauth_authenticate, + ), patch("pymyq.api.API.request", side_effect=_handle_mock_api_request): entry = MockConfigEntry( domain=DOMAIN, data={CONF_USERNAME: "mock", CONF_PASSWORD: "mock"} )