Remove MyQ Integration (#103565)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
pull/103594/head
Luke Lashley 2023-11-07 08:11:54 -05:00 committed by GitHub
parent 38acad8263
commit c13744f4cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 79 additions and 1037 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,
)

View File

@ -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

View File

@ -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")

View File

@ -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()

View File

@ -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": []
}

View File

@ -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})."
}
}
}

View File

@ -301,7 +301,6 @@ FLOWS = {
"mqtt",
"mullvad",
"mutesync",
"myq",
"mysensors",
"mystrom",
"nam",

View File

@ -316,10 +316,6 @@ DHCP: list[dict[str, str | bool]] = [
"domain": "motion_blinds",
"hostname": "connector_*",
},
{
"domain": "myq",
"macaddress": "645299*",
},
{
"domain": "nest",
"macaddress": "18B430*",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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"
}
]
}

View File

@ -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
)

View File

@ -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"

View File

@ -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
)

View File

@ -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

View File

@ -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
)

View File

@ -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