Remove MyQ Integration (#103565)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
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/mutesync/binary_sensor.py
homeassistant/components/mvglive/sensor.py homeassistant/components/mvglive/sensor.py
homeassistant/components/mycroft/* 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/__init__.py
homeassistant/components/mysensors/climate.py homeassistant/components/mysensors/climate.py
homeassistant/components/mysensors/cover.py homeassistant/components/mysensors/cover.py

View file

@ -811,8 +811,6 @@ build.json @home-assistant/supervisor
/tests/components/mutesync/ @currentoor /tests/components/mutesync/ @currentoor
/homeassistant/components/my/ @home-assistant/core /homeassistant/components/my/ @home-assistant/core
/tests/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 /homeassistant/components/mysensors/ @MartinHjelmare @functionpointer
/tests/components/mysensors/ @MartinHjelmare @functionpointer /tests/components/mysensors/ @MartinHjelmare @functionpointer
/homeassistant/components/mystrom/ @fabaff /homeassistant/components/mystrom/ @fabaff

View file

@ -1,122 +1,38 @@
"""The MyQ integration.""" """The MyQ integration."""
from __future__ import annotations from __future__ import annotations
from datetime import timedelta from homeassistant.config_entries import ConfigEntry, ConfigEntryState
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.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, PLATFORMS, UPDATE_INTERVAL DOMAIN = "myq"
_LOGGER = logging.getLogger(__name__)
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.""" """Set up MyQ from a config entry."""
ir.async_create_issue(
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(
hass, hass,
_LOGGER, DOMAIN,
name="myq devices", DOMAIN,
update_method=async_update_data, is_fixable=False,
update_interval=timedelta(seconds=UPDATE_INTERVAL), 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 return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if all(
if unload_ok: config_entry.state is ConfigEntryState.NOT_LOADED
hass.data[DOMAIN].pop(entry.entry_id) 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 return True
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
)

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.""" """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 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 from . import DOMAIN
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema(
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
)
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for MyQ.""" """Handle a config flow for MyQ."""
VERSION = 1 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", "domain": "myq",
"name": "MyQ", "name": "MyQ",
"codeowners": ["@ehendrix23", "@Lash-L"], "codeowners": [],
"config_flow": true,
"dhcp": [
{
"macaddress": "645299*"
}
],
"documentation": "https://www.home-assistant.io/integrations/myq", "documentation": "https://www.home-assistant.io/integrations/myq",
"homekit": { "integration_type": "system",
"models": ["819LMB", "MYQ"]
},
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pkce", "pymyq"], "requirements": []
"requirements": ["python-myq==3.1.13"]
} }

View file

@ -1,29 +1,8 @@
{ {
"config": { "issues": {
"step": { "integration_removed": {
"user": { "title": "The MyQ integration has been removed",
"title": "Connect to the MyQ Gateway", "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})."
"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%]"
} }
} }
} }

View file

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

View file

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

View file

@ -3625,12 +3625,6 @@
"config_flow": false, "config_flow": false,
"iot_class": "local_push" "iot_class": "local_push"
}, },
"myq": {
"name": "MyQ",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
"mysensors": { "mysensors": {
"name": "MySensors", "name": "MySensors",
"integration_type": "hub", "integration_type": "hub",

View file

@ -20,10 +20,6 @@ HOMEKIT = {
"always_discover": True, "always_discover": True,
"domain": "roku", "domain": "roku",
}, },
"819LMB": {
"always_discover": True,
"domain": "myq",
},
"AC02": { "AC02": {
"always_discover": True, "always_discover": True,
"domain": "tado", "domain": "tado",
@ -144,10 +140,6 @@ HOMEKIT = {
"always_discover": True, "always_discover": True,
"domain": "lifx", "domain": "lifx",
}, },
"MYQ": {
"always_discover": True,
"domain": "myq",
},
"NL29": { "NL29": {
"always_discover": False, "always_discover": False,
"domain": "nanoleaf", "domain": "nanoleaf",

View file

@ -528,8 +528,6 @@ filterwarnings = [
"ignore:datetime.*utcfromtimestamp\\(\\) is deprecated:DeprecationWarning:proto.datetime_helpers", "ignore:datetime.*utcfromtimestamp\\(\\) is deprecated:DeprecationWarning:proto.datetime_helpers",
# https://github.com/MatsNl/pyatag/issues/11 - v0.3.7.1 # https://github.com/MatsNl/pyatag/issues/11 - v0.3.7.1
"ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:pyatag.gateway", "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 # Wrong stacklevel
# https://bugs.launchpad.net/beautifulsoup/+bug/2034451 # https://bugs.launchpad.net/beautifulsoup/+bug/2034451
"ignore:It looks like you're parsing an XML document using an HTML parser:UserWarning:bs4.builder", "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 # homeassistant.components.mpd
python-mpd2==3.0.5 python-mpd2==3.0.5
# homeassistant.components.myq
python-myq==3.1.13
# homeassistant.components.mystrom # homeassistant.components.mystrom
python-mystrom==2.2.0 python-mystrom==2.2.0

View file

@ -1611,9 +1611,6 @@ python-matter-server==4.0.0
# homeassistant.components.xiaomi_miio # homeassistant.components.xiaomi_miio
python-miio==0.5.12 python-miio==0.5.12
# homeassistant.components.myq
python-myq==3.1.13
# homeassistant.components.mystrom # homeassistant.components.mystrom
python-mystrom==2.2.0 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