diff --git a/.coveragerc b/.coveragerc index e41d668ed56..99685df79d7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -769,9 +769,6 @@ omit = homeassistant/components/mutesync/binary_sensor.py homeassistant/components/mvglive/sensor.py homeassistant/components/mycroft/* - homeassistant/components/myq/__init__.py - homeassistant/components/myq/cover.py - homeassistant/components/myq/light.py homeassistant/components/mysensors/__init__.py homeassistant/components/mysensors/climate.py homeassistant/components/mysensors/cover.py diff --git a/CODEOWNERS b/CODEOWNERS index b77916932d1..0a594d71b77 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -811,8 +811,6 @@ build.json @home-assistant/supervisor /tests/components/mutesync/ @currentoor /homeassistant/components/my/ @home-assistant/core /tests/components/my/ @home-assistant/core -/homeassistant/components/myq/ @ehendrix23 @Lash-L -/tests/components/myq/ @ehendrix23 @Lash-L /homeassistant/components/mysensors/ @MartinHjelmare @functionpointer /tests/components/mysensors/ @MartinHjelmare @functionpointer /homeassistant/components/mystrom/ @fabaff diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py index c50ea579a14..86a158c09fa 100644 --- a/homeassistant/components/myq/__init__.py +++ b/homeassistant/components/myq/__init__.py @@ -1,122 +1,38 @@ """The MyQ integration.""" from __future__ import annotations -from datetime import timedelta -import logging - -import pymyq -from pymyq.const import ( - DEVICE_STATE as MYQ_DEVICE_STATE, - DEVICE_STATE_ONLINE as MYQ_DEVICE_STATE_ONLINE, - KNOWN_MODELS, - MANUFACTURER, -) -from pymyq.device import MyQDevice -from pymyq.errors import InvalidCredentialsError, MyQError - -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) +from homeassistant.helpers import issue_registry as ir -from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, PLATFORMS, UPDATE_INTERVAL - -_LOGGER = logging.getLogger(__name__) +DOMAIN = "myq" -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool: """Set up MyQ from a config entry.""" - - hass.data.setdefault(DOMAIN, {}) - websession = aiohttp_client.async_get_clientsession(hass) - conf = entry.data - - try: - myq = await pymyq.login(conf[CONF_USERNAME], conf[CONF_PASSWORD], websession) - except InvalidCredentialsError as err: - raise ConfigEntryAuthFailed from err - 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 InvalidCredentialsError as err: - raise ConfigEntryAuthFailed from err - except MyQError as err: - raise UpdateFailed(str(err)) from err - - coordinator = DataUpdateCoordinator( + ir.async_create_issue( hass, - _LOGGER, - name="myq devices", - update_method=async_update_data, - update_interval=timedelta(seconds=UPDATE_INTERVAL), + DOMAIN, + DOMAIN, + is_fixable=False, + severity=ir.IssueSeverity.ERROR, + translation_key="integration_removed", + translation_placeholders={ + "blog": "https://www.home-assistant.io/blog/2023/11/06/removal-of-myq-integration/", + "entries": "/config/integrations/integration/myQ", + }, ) - hass.data[DOMAIN][entry.entry_id] = {MYQ_GATEWAY: myq, MYQ_COORDINATOR: coordinator} - - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) + if all( + config_entry.state is ConfigEntryState.NOT_LOADED + for config_entry in hass.config_entries.async_entries(DOMAIN) + if config_entry.entry_id != entry.entry_id + ): + ir.async_delete_issue(hass, DOMAIN, DOMAIN) - return unload_ok - - -class MyQEntity(CoordinatorEntity): - """Base class for MyQ Entities.""" - - def __init__(self, coordinator: DataUpdateCoordinator, device: MyQDevice) -> None: - """Initialize class.""" - super().__init__(coordinator) - self._device = device - self._attr_unique_id = device.device_id - - @property - def name(self): - """Return the name if any, name can change if user changes it within MyQ.""" - return self._device.name - - @property - def device_info(self): - """Return the device_info of the device.""" - model = ( - KNOWN_MODELS.get(self._device.device_id[2:4]) - if self._device.device_id is not None - else None - ) - via_device: tuple[str, str] | None = None - if self._device.parent_device_id: - via_device = (DOMAIN, self._device.parent_device_id) - return DeviceInfo( - identifiers={(DOMAIN, self._device.device_id)}, - manufacturer=MANUFACTURER, - model=model, - name=self._device.name, - sw_version=self._device.firmware_version, - via_device=via_device, - ) - - @property - def available(self): - """Return if the device is online.""" - # Not all devices report online so assume True if its missing - return super().available and self._device.device_json[MYQ_DEVICE_STATE].get( - MYQ_DEVICE_STATE_ONLINE, True - ) + return True diff --git a/homeassistant/components/myq/binary_sensor.py b/homeassistant/components/myq/binary_sensor.py deleted file mode 100644 index f4c976a5879..00000000000 --- a/homeassistant/components/myq/binary_sensor.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Support for MyQ gateways.""" -from homeassistant.components.binary_sensor import ( - BinarySensorDeviceClass, - BinarySensorEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EntityCategory -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import MyQEntity -from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up mysq covers.""" - data = hass.data[DOMAIN][config_entry.entry_id] - myq = data[MYQ_GATEWAY] - coordinator = data[MYQ_COORDINATOR] - - entities = [] - - for device in myq.gateways.values(): - entities.append(MyQBinarySensorEntity(coordinator, device)) - - async_add_entities(entities) - - -class MyQBinarySensorEntity(MyQEntity, BinarySensorEntity): - """Representation of a MyQ gateway.""" - - _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY - _attr_entity_category = EntityCategory.DIAGNOSTIC - - @property - def name(self): - """Return the name of the garage door if any.""" - return f"{self._device.name} MyQ Gateway" - - @property - def is_on(self): - """Return if the device is online.""" - return super().available - - @property - def available(self) -> bool: - """Entity is always available.""" - return True diff --git a/homeassistant/components/myq/config_flow.py b/homeassistant/components/myq/config_flow.py index 930d0014d1f..27bb1c4b9e5 100644 --- a/homeassistant/components/myq/config_flow.py +++ b/homeassistant/components/myq/config_flow.py @@ -1,101 +1,11 @@ """Config flow for MyQ integration.""" -from collections.abc import Mapping -import logging -from typing import Any - -import pymyq -from pymyq.errors import InvalidCredentialsError, MyQError -import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers import aiohttp_client -from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) - -DATA_SCHEMA = vol.Schema( - {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} -) +from . import DOMAIN class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for MyQ.""" VERSION = 1 - - def __init__(self) -> None: - """Start a myq config flow.""" - self._reauth_unique_id = None - - async def _async_validate_input(self, username, password): - """Validate the user input allows us to connect.""" - websession = aiohttp_client.async_get_clientsession(self.hass) - try: - await pymyq.login(username, password, websession, True) - except InvalidCredentialsError: - return {CONF_PASSWORD: "invalid_auth"} - except MyQError: - return {"base": "cannot_connect"} - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - return {"base": "unknown"} - - return None - - async def async_step_user(self, user_input=None): - """Handle the initial step.""" - errors = {} - if user_input is not None: - errors = await self._async_validate_input( - user_input[CONF_USERNAME], user_input[CONF_PASSWORD] - ) - if not errors: - await self.async_set_unique_id(user_input[CONF_USERNAME]) - self._abort_if_unique_id_configured() - return self.async_create_entry( - title=user_input[CONF_USERNAME], data=user_input - ) - - return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors - ) - - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: - """Handle reauth.""" - self._reauth_unique_id = self.context["unique_id"] - return await self.async_step_reauth_confirm() - - async def async_step_reauth_confirm(self, user_input=None): - """Handle reauth input.""" - errors = {} - existing_entry = await self.async_set_unique_id(self._reauth_unique_id) - if user_input is not None: - errors = await self._async_validate_input( - existing_entry.data[CONF_USERNAME], user_input[CONF_PASSWORD] - ) - if not errors: - self.hass.config_entries.async_update_entry( - existing_entry, - data={ - **existing_entry.data, - CONF_PASSWORD: user_input[CONF_PASSWORD], - }, - ) - await self.hass.config_entries.async_reload(existing_entry.entry_id) - return self.async_abort(reason="reauth_successful") - - return self.async_show_form( - description_placeholders={ - CONF_USERNAME: existing_entry.data[CONF_USERNAME] - }, - step_id="reauth_confirm", - data_schema=vol.Schema( - { - vol.Required(CONF_PASSWORD): str, - } - ), - errors=errors, - ) diff --git a/homeassistant/components/myq/const.py b/homeassistant/components/myq/const.py deleted file mode 100644 index 16dead34477..00000000000 --- a/homeassistant/components/myq/const.py +++ /dev/null @@ -1,36 +0,0 @@ -"""The MyQ integration.""" -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 pymyq.lamp import STATE_OFF as MYQ_LIGHT_STATE_OFF, STATE_ON as MYQ_LIGHT_STATE_ON - -from homeassistant.const import ( - STATE_CLOSED, - STATE_CLOSING, - STATE_OFF, - STATE_ON, - STATE_OPEN, - STATE_OPENING, - Platform, -) - -DOMAIN = "myq" - -PLATFORMS = [Platform.COVER, Platform.BINARY_SENSOR, Platform.LIGHT] - -MYQ_TO_HASS = { - 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_LIGHT_STATE_ON: STATE_ON, - MYQ_LIGHT_STATE_OFF: STATE_OFF, -} - -MYQ_GATEWAY = "myq_gateway" -MYQ_COORDINATOR = "coordinator" - -UPDATE_INTERVAL = 30 diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py deleted file mode 100644 index 51d0b3290a6..00000000000 --- a/homeassistant/components/myq/cover.py +++ /dev/null @@ -1,116 +0,0 @@ -"""Support for MyQ-Enabled Garage Doors.""" -from typing import Any - -from pymyq.const import DEVICE_TYPE_GATE as MYQ_DEVICE_TYPE_GATE -from pymyq.errors import MyQError - -from homeassistant.components.cover import ( - CoverDeviceClass, - CoverEntity, - CoverEntityFeature, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import MyQEntity -from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, MYQ_TO_HASS - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up mysq covers.""" - data = hass.data[DOMAIN][config_entry.entry_id] - myq = data[MYQ_GATEWAY] - coordinator = data[MYQ_COORDINATOR] - - async_add_entities( - [MyQCover(coordinator, device) for device in myq.covers.values()] - ) - - -class MyQCover(MyQEntity, CoverEntity): - """Representation of a MyQ cover.""" - - _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE - - def __init__(self, coordinator, device): - """Initialize with API object, device id.""" - super().__init__(coordinator, device) - self._device = device - if device.device_type == MYQ_DEVICE_TYPE_GATE: - self._attr_device_class = CoverDeviceClass.GATE - else: - self._attr_device_class = CoverDeviceClass.GARAGE - self._attr_unique_id = device.device_id - - @property - def is_closed(self) -> bool: - """Return true if cover is closed, else False.""" - return MYQ_TO_HASS.get(self._device.state) == STATE_CLOSED - - @property - def is_closing(self) -> bool: - """Return if the cover is closing or not.""" - return MYQ_TO_HASS.get(self._device.state) == STATE_CLOSING - - @property - def is_open(self) -> bool: - """Return if the cover is opening or not.""" - return MYQ_TO_HASS.get(self._device.state) == STATE_OPEN - - @property - def is_opening(self) -> bool: - """Return if the cover is opening or not.""" - return MYQ_TO_HASS.get(self._device.state) == STATE_OPENING - - async def async_close_cover(self, **kwargs: Any) -> None: - """Issue close command to cover.""" - if self.is_closing or self.is_closed: - return - - try: - wait_task = await self._device.close(wait_for_state=False) - except MyQError as err: - raise HomeAssistantError( - f"Closing of cover {self._device.name} failed with error: {err}" - ) from err - - # Write closing state to HASS - self.async_write_ha_state() - - result = wait_task if isinstance(wait_task, bool) else await wait_task - - # Write final state to HASS - self.async_write_ha_state() - - if not result: - raise HomeAssistantError(f"Closing of cover {self._device.name} failed") - - async def async_open_cover(self, **kwargs: Any) -> None: - """Issue open command to cover.""" - if self.is_opening or self.is_open: - return - - try: - wait_task = await self._device.open(wait_for_state=False) - except MyQError as err: - raise HomeAssistantError( - f"Opening of cover {self._device.name} failed with error: {err}" - ) from err - - # Write opening state to HASS - self.async_write_ha_state() - - result = wait_task if isinstance(wait_task, bool) else await wait_task - - # Write final state to HASS - self.async_write_ha_state() - - if not result: - raise HomeAssistantError(f"Opening of cover {self._device.name} failed") diff --git a/homeassistant/components/myq/light.py b/homeassistant/components/myq/light.py deleted file mode 100644 index 684af64a82e..00000000000 --- a/homeassistant/components/myq/light.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Support for MyQ-Enabled lights.""" -from typing import Any - -from pymyq.errors import MyQError - -from homeassistant.components.light import ColorMode, LightEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import MyQEntity -from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, MYQ_TO_HASS - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up myq lights.""" - data = hass.data[DOMAIN][config_entry.entry_id] - myq = data[MYQ_GATEWAY] - coordinator = data[MYQ_COORDINATOR] - - async_add_entities( - [MyQLight(coordinator, device) for device in myq.lamps.values()], True - ) - - -class MyQLight(MyQEntity, LightEntity): - """Representation of a MyQ light.""" - - _attr_color_mode = ColorMode.ONOFF - _attr_supported_color_modes = {ColorMode.ONOFF} - - @property - def is_on(self): - """Return true if the light is on, else False.""" - return MYQ_TO_HASS.get(self._device.state) == STATE_ON - - @property - def is_off(self): - """Return true if the light is off, else False.""" - return MYQ_TO_HASS.get(self._device.state) == STATE_OFF - - async def async_turn_on(self, **kwargs: Any) -> None: - """Issue on command to light.""" - if self.is_on: - return - - try: - await self._device.turnon(wait_for_state=True) - except MyQError as err: - raise HomeAssistantError( - f"Turning light {self._device.name} on failed with error: {err}" - ) from err - - # Write new state to HASS - self.async_write_ha_state() - - async def async_turn_off(self, **kwargs: Any) -> None: - """Issue off command to light.""" - if self.is_off: - return - - try: - await self._device.turnoff(wait_for_state=True) - except MyQError as err: - raise HomeAssistantError( - f"Turning light {self._device.name} off failed with error: {err}" - ) from err - - # Write new state to HASS - self.async_write_ha_state() diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index e924d06955b..dd265c4a428 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -1,18 +1,9 @@ { "domain": "myq", "name": "MyQ", - "codeowners": ["@ehendrix23", "@Lash-L"], - "config_flow": true, - "dhcp": [ - { - "macaddress": "645299*" - } - ], + "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/myq", - "homekit": { - "models": ["819LMB", "MYQ"] - }, + "integration_type": "system", "iot_class": "cloud_polling", - "loggers": ["pkce", "pymyq"], - "requirements": ["python-myq==3.1.13"] + "requirements": [] } diff --git a/homeassistant/components/myq/strings.json b/homeassistant/components/myq/strings.json index c986b8a8997..85359302c99 100644 --- a/homeassistant/components/myq/strings.json +++ b/homeassistant/components/myq/strings.json @@ -1,29 +1,8 @@ { - "config": { - "step": { - "user": { - "title": "Connect to the MyQ Gateway", - "data": { - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" - } - }, - "reauth_confirm": { - "description": "The password for {username} is no longer valid.", - "title": "Reauthenticate your MyQ Account", - "data": { - "password": "[%key:common::config_flow::data::password%]" - } - } - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "abort": { - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + "issues": { + "integration_removed": { + "title": "The MyQ integration has been removed", + "description": "The MyQ integration has been removed from Home Assistant.\n\nMyQ has blocked all third-party integrations. Read about it [here]({blog}).\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing MyQ integration entries]({entries})." } } } diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b7f112783ad..4cd6a93d83a 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -301,7 +301,6 @@ FLOWS = { "mqtt", "mullvad", "mutesync", - "myq", "mysensors", "mystrom", "nam", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index bc73c1b9804..63c7cd84303 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -316,10 +316,6 @@ DHCP: list[dict[str, str | bool]] = [ "domain": "motion_blinds", "hostname": "connector_*", }, - { - "domain": "myq", - "macaddress": "645299*", - }, { "domain": "nest", "macaddress": "18B430*", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index dca042bed20..475a1c47eb2 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3625,12 +3625,6 @@ "config_flow": false, "iot_class": "local_push" }, - "myq": { - "name": "MyQ", - "integration_type": "hub", - "config_flow": true, - "iot_class": "cloud_polling" - }, "mysensors": { "name": "MySensors", "integration_type": "hub", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 36ddfd68479..485d16e46e7 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -20,10 +20,6 @@ HOMEKIT = { "always_discover": True, "domain": "roku", }, - "819LMB": { - "always_discover": True, - "domain": "myq", - }, "AC02": { "always_discover": True, "domain": "tado", @@ -144,10 +140,6 @@ HOMEKIT = { "always_discover": True, "domain": "lifx", }, - "MYQ": { - "always_discover": True, - "domain": "myq", - }, "NL29": { "always_discover": False, "domain": "nanoleaf", diff --git a/pyproject.toml b/pyproject.toml index 60557c3948e..5c3bee507ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -528,8 +528,6 @@ filterwarnings = [ "ignore:datetime.*utcfromtimestamp\\(\\) is deprecated:DeprecationWarning:proto.datetime_helpers", # https://github.com/MatsNl/pyatag/issues/11 - v0.3.7.1 "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:pyatag.gateway", - # https://github.com/Python-MyQ/Python-MyQ - v3.1.13 - "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:pymyq.(api|account)", # Wrong stacklevel # https://bugs.launchpad.net/beautifulsoup/+bug/2034451 "ignore:It looks like you're parsing an XML document using an HTML parser:UserWarning:bs4.builder", diff --git a/requirements_all.txt b/requirements_all.txt index 1fa8c90a7b6..c2283c5310b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2161,9 +2161,6 @@ python-miio==0.5.12 # homeassistant.components.mpd python-mpd2==3.0.5 -# homeassistant.components.myq -python-myq==3.1.13 - # homeassistant.components.mystrom python-mystrom==2.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dedeeef4c47..0b9220e21f4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1611,9 +1611,6 @@ python-matter-server==4.0.0 # homeassistant.components.xiaomi_miio python-miio==0.5.12 -# homeassistant.components.myq -python-myq==3.1.13 - # homeassistant.components.mystrom python-mystrom==2.2.0 diff --git a/tests/components/myq/fixtures/devices.json b/tests/components/myq/fixtures/devices.json deleted file mode 100644 index 0966845e3ca..00000000000 --- a/tests/components/myq/fixtures/devices.json +++ /dev/null @@ -1,163 +0,0 @@ -{ - "count": 6, - "href": "http://api.myqdevice.com/api/v5/accounts/account_id/devices", - "items": [ - { - "device_type": "ethernetgateway", - "created_date": "2020-02-10T22:54:58.423", - "href": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gateway_serial", - "device_family": "gateway", - "name": "Happy place", - "device_platform": "myq", - "state": { - "homekit_enabled": false, - "pending_bootload_abandoned": false, - "online": true, - "last_status": "2020-03-30T02:49:46.4121303Z", - "physical_devices": [], - "firmware_version": "1.6", - "learn_mode": false, - "learn": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gateway_serial/learn", - "homekit_capable": false, - "updated_date": "2020-03-30T02:49:46.4171299Z" - }, - "serial_number": "gateway_serial" - }, - { - "serial_number": "gate_serial", - "state": { - "report_ajar": false, - "aux_relay_delay": "00:00:00", - "is_unattended_close_allowed": true, - "door_ajar_interval": "00:00:00", - "aux_relay_behavior": "None", - "last_status": "2020-03-30T02:47:40.2794038Z", - "online": true, - "rex_fires_door": false, - "close": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gate_serial/close", - "invalid_shutout_period": "00:00:00", - "invalid_credential_window": "00:00:00", - "use_aux_relay": false, - "command_channel_report_status": false, - "last_update": "2020-03-28T23:07:39.5611776Z", - "door_state": "closed", - "max_invalid_attempts": 0, - "open": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gate_serial/open", - "passthrough_interval": "00:00:00", - "control_from_browser": false, - "report_forced": false, - "is_unattended_open_allowed": true - }, - "parent_device_id": "gateway_serial", - "name": "Gate", - "device_platform": "myq", - "device_family": "garagedoor", - "parent_device": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gateway_serial", - "href": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gate_serial", - "device_type": "gate", - "created_date": "2020-02-10T22:54:58.423" - }, - { - "parent_device": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gateway_serial", - "href": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/large_garage_serial", - "device_type": "wifigaragedooropener", - "created_date": "2020-02-10T22:55:25.863", - "device_platform": "myq", - "name": "Large Garage Door", - "device_family": "garagedoor", - "serial_number": "large_garage_serial", - "state": { - "report_forced": false, - "is_unattended_open_allowed": true, - "passthrough_interval": "00:00:00", - "control_from_browser": false, - "attached_work_light_error_present": false, - "max_invalid_attempts": 0, - "open": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/large_garage_serial/open", - "command_channel_report_status": false, - "last_update": "2020-03-28T23:58:55.5906643Z", - "door_state": "closed", - "invalid_shutout_period": "00:00:00", - "use_aux_relay": false, - "invalid_credential_window": "00:00:00", - "rex_fires_door": false, - "close": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/large_garage_serial/close", - "online": true, - "last_status": "2020-03-30T02:49:46.4121303Z", - "aux_relay_behavior": "None", - "door_ajar_interval": "00:00:00", - "gdo_lock_connected": false, - "report_ajar": false, - "aux_relay_delay": "00:00:00", - "is_unattended_close_allowed": true - }, - "parent_device_id": "gateway_serial" - }, - { - "serial_number": "small_garage_serial", - "state": { - "last_status": "2020-03-30T02:48:45.7501595Z", - "online": true, - "report_ajar": false, - "aux_relay_delay": "00:00:00", - "is_unattended_close_allowed": true, - "gdo_lock_connected": false, - "door_ajar_interval": "00:00:00", - "aux_relay_behavior": "None", - "attached_work_light_error_present": false, - "control_from_browser": false, - "passthrough_interval": "00:00:00", - "is_unattended_open_allowed": true, - "report_forced": false, - "close": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/small_garage_serial/close", - "rex_fires_door": false, - "invalid_credential_window": "00:00:00", - "use_aux_relay": false, - "invalid_shutout_period": "00:00:00", - "door_state": "closed", - "last_update": "2020-03-26T15:45:31.4713796Z", - "command_channel_report_status": false, - "open": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/small_garage_serial/open", - "max_invalid_attempts": 0 - }, - "parent_device_id": "gateway_serial", - "device_platform": "myq", - "name": "Small Garage Door", - "device_family": "garagedoor", - "parent_device": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gateway_serial", - "href": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/small_garage_serial", - "device_type": "wifigaragedooropener", - "created_date": "2020-02-10T23:11:47.487" - }, - { - "serial_number": "garage_light_off", - "state": { - "last_status": "2020-03-30T02:48:45.7501595Z", - "online": true, - "lamp_state": "off", - "last_update": "2020-03-26T15:45:31.4713796Z" - }, - "parent_device_id": "gateway_serial", - "device_platform": "myq", - "name": "Garage Door Light Off", - "device_family": "lamp", - "device_type": "lamp", - "created_date": "2020-02-10T23:11:47.487" - }, - { - "serial_number": "garage_light_on", - "state": { - "last_status": "2020-03-30T02:48:45.7501595Z", - "online": true, - "lamp_state": "on", - "last_update": "2020-03-26T15:45:31.4713796Z" - }, - "parent_device_id": "gateway_serial", - "device_platform": "myq", - "name": "Garage Door Light On", - "device_family": "lamp", - "device_type": "lamp", - "created_date": "2020-02-10T23:11:47.487" - } - ] -} diff --git a/tests/components/myq/test_binary_sensor.py b/tests/components/myq/test_binary_sensor.py deleted file mode 100644 index 39a2a4dff3a..00000000000 --- a/tests/components/myq/test_binary_sensor.py +++ /dev/null @@ -1,20 +0,0 @@ -"""The scene tests for the myq platform.""" -from homeassistant.const import STATE_ON -from homeassistant.core import HomeAssistant - -from .util import async_init_integration - - -async def test_create_binary_sensors(hass: HomeAssistant) -> None: - """Test creation of binary_sensors.""" - - await async_init_integration(hass) - - state = hass.states.get("binary_sensor.happy_place_myq_gateway") - assert state.state == STATE_ON - expected_attributes = {"device_class": "connectivity"} - # Only test for a subset of attributes in case - # HA changes the implementation and a new one appears - assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes - ) diff --git a/tests/components/myq/test_config_flow.py b/tests/components/myq/test_config_flow.py deleted file mode 100644 index 2df69168852..00000000000 --- a/tests/components/myq/test_config_flow.py +++ /dev/null @@ -1,166 +0,0 @@ -"""Test the MyQ config flow.""" -from unittest.mock import patch - -from pymyq.errors import InvalidCredentialsError, MyQError - -from homeassistant import config_entries -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 - - -async def test_form_user(hass: HomeAssistant) -> None: - """Test we get the user form.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == "form" - assert result["errors"] == {} - - with patch( - "homeassistant.components.myq.config_flow.pymyq.login", - return_value=True, - ), patch( - "homeassistant.components.myq.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"username": "test-username", "password": "test-password"}, - ) - await hass.async_block_till_done() - - assert result2["type"] == "create_entry" - assert result2["title"] == "test-username" - assert result2["data"] == { - "username": "test-username", - "password": "test-password", - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_form_invalid_auth(hass: HomeAssistant) -> None: - """Test we handle invalid auth.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.myq.config_flow.pymyq.login", - side_effect=InvalidCredentialsError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"username": "test-username", "password": "test-password"}, - ) - - assert result2["type"] == "form" - assert result2["errors"] == {"password": "invalid_auth"} - - -async def test_form_cannot_connect(hass: HomeAssistant) -> None: - """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.myq.config_flow.pymyq.login", - side_effect=MyQError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"username": "test-username", "password": "test-password"}, - ) - - assert result2["type"] == "form" - assert result2["errors"] == {"base": "cannot_connect"} - - -async def test_form_unknown_exception(hass: HomeAssistant) -> None: - """Test we handle unknown exceptions.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.myq.config_flow.pymyq.login", - side_effect=Exception, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"username": "test-username", "password": "test-password"}, - ) - - assert result2["type"] == "form" - assert result2["errors"] == {"base": "unknown"} - - -async def test_reauth(hass: HomeAssistant) -> None: - """Test we can reauth.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_USERNAME: "test@test.org", - CONF_PASSWORD: "secret", - }, - unique_id="test@test.org", - ) - entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_REAUTH, "unique_id": "test@test.org"}, - ) - - assert result["type"] == "form" - assert result["step_id"] == "reauth_confirm" - - with patch( - "homeassistant.components.myq.config_flow.pymyq.login", - side_effect=InvalidCredentialsError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_PASSWORD: "test-password", - }, - ) - - assert result2["type"] == "form" - assert result2["errors"] == {"password": "invalid_auth"} - - with patch( - "homeassistant.components.myq.config_flow.pymyq.login", - side_effect=MyQError, - ): - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - { - CONF_PASSWORD: "test-password", - }, - ) - - assert result3["type"] == "form" - assert result3["errors"] == {"base": "cannot_connect"} - - with patch( - "homeassistant.components.myq.config_flow.pymyq.login", - return_value=True, - ), patch( - "homeassistant.components.myq.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result4 = await hass.config_entries.flow.async_configure( - result3["flow_id"], - { - CONF_PASSWORD: "test-password", - }, - ) - - assert mock_setup_entry.called - assert result4["type"] == "abort" - assert result4["reason"] == "reauth_successful" diff --git a/tests/components/myq/test_cover.py b/tests/components/myq/test_cover.py deleted file mode 100644 index b8d6cf53736..00000000000 --- a/tests/components/myq/test_cover.py +++ /dev/null @@ -1,50 +0,0 @@ -"""The scene tests for the myq platform.""" -from homeassistant.const import STATE_CLOSED -from homeassistant.core import HomeAssistant - -from .util import async_init_integration - - -async def test_create_covers(hass: HomeAssistant) -> None: - """Test creation of covers.""" - - await async_init_integration(hass) - - state = hass.states.get("cover.large_garage_door") - assert state.state == STATE_CLOSED - expected_attributes = { - "device_class": "garage", - "friendly_name": "Large Garage Door", - "supported_features": 3, - } - # Only test for a subset of attributes in case - # HA changes the implementation and a new one appears - assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes - ) - - state = hass.states.get("cover.small_garage_door") - assert state.state == STATE_CLOSED - expected_attributes = { - "device_class": "garage", - "friendly_name": "Small Garage Door", - "supported_features": 3, - } - # Only test for a subset of attributes in case - # HA changes the implementation and a new one appears - assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes - ) - - state = hass.states.get("cover.gate") - assert state.state == STATE_CLOSED - expected_attributes = { - "device_class": "gate", - "friendly_name": "Gate", - "supported_features": 3, - } - # Only test for a subset of attributes in case - # HA changes the implementation and a new one appears - assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes - ) diff --git a/tests/components/myq/test_init.py b/tests/components/myq/test_init.py new file mode 100644 index 00000000000..24e03f56075 --- /dev/null +++ b/tests/components/myq/test_init.py @@ -0,0 +1,50 @@ +"""Tests for the MyQ Connected Services integration.""" + +from homeassistant.components.myq import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir + +from tests.common import MockConfigEntry + + +async def test_myq_repair_issue( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: + """Test the MyQ configuration entry loading/unloading handles the repair.""" + config_entry_1 = MockConfigEntry( + title="Example 1", + domain=DOMAIN, + ) + config_entry_1.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry_1.entry_id) + await hass.async_block_till_done() + assert config_entry_1.state is ConfigEntryState.LOADED + + # Add a second one + config_entry_2 = MockConfigEntry( + title="Example 2", + domain=DOMAIN, + ) + config_entry_2.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry_2.entry_id) + await hass.async_block_till_done() + + assert config_entry_2.state is ConfigEntryState.LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) + + # Remove the first one + await hass.config_entries.async_remove(config_entry_1.entry_id) + await hass.async_block_till_done() + + assert config_entry_1.state is ConfigEntryState.NOT_LOADED + assert config_entry_2.state is ConfigEntryState.LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) + + # Remove the second one + await hass.config_entries.async_remove(config_entry_2.entry_id) + await hass.async_block_till_done() + + assert config_entry_1.state is ConfigEntryState.NOT_LOADED + assert config_entry_2.state is ConfigEntryState.NOT_LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) is None diff --git a/tests/components/myq/test_light.py b/tests/components/myq/test_light.py deleted file mode 100644 index ca80e768779..00000000000 --- a/tests/components/myq/test_light.py +++ /dev/null @@ -1,39 +0,0 @@ -"""The scene tests for the myq platform.""" -from homeassistant.components.light import ColorMode -from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.core import HomeAssistant - -from .util import async_init_integration - - -async def test_create_lights(hass: HomeAssistant) -> None: - """Test creation of lights.""" - - await async_init_integration(hass) - - state = hass.states.get("light.garage_door_light_off") - assert state.state == STATE_OFF - expected_attributes = { - "friendly_name": "Garage Door Light Off", - "supported_features": 0, - "supported_color_modes": [ColorMode.ONOFF], - } - # Only test for a subset of attributes in case - # HA changes the implementation and a new one appears - assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes - ) - - state = hass.states.get("light.garage_door_light_on") - assert state.state == STATE_ON - expected_attributes = { - "friendly_name": "Garage Door Light On", - "supported_features": 0, - "supported_color_modes": [ColorMode.ONOFF], - } - # Only test for a subset of attributes in case - # HA changes the implementation and a new one appears - - assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes - ) diff --git a/tests/components/myq/util.py b/tests/components/myq/util.py deleted file mode 100644 index 8cb0d17f592..00000000000 --- a/tests/components/myq/util.py +++ /dev/null @@ -1,54 +0,0 @@ -"""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, - skip_setup: bool = False, -) -> MockConfigEntry: - """Set up the myq integration in Home Assistant.""" - - devices_fixture = "myq/devices.json" - devices_json = load_fixture(devices_fixture) - devices_dict = json.loads(devices_json) - - def _handle_mock_api_oauth_authenticate(): - return 1234, 1800 - - 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"} - ) - entry.add_to_hass(hass) - - if not skip_setup: - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - return entry