Honeywell Lyric Integration (#39695)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Aidan Timson 2021-01-31 17:51:31 +00:00 committed by GitHub
parent ee55223065
commit 2d10c83150
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 799 additions and 0 deletions

View File

@ -518,6 +518,9 @@ omit =
homeassistant/components/lutron_caseta/switch.py
homeassistant/components/lw12wifi/light.py
homeassistant/components/lyft/sensor.py
homeassistant/components/lyric/__init__.py
homeassistant/components/lyric/api.py
homeassistant/components/lyric/climate.py
homeassistant/components/magicseaweed/sensor.py
homeassistant/components/mailgun/notify.py
homeassistant/components/map/*

View File

@ -260,6 +260,7 @@ homeassistant/components/luftdaten/* @fabaff
homeassistant/components/lupusec/* @majuss
homeassistant/components/lutron/* @JonGilmore
homeassistant/components/lutron_caseta/* @swails @bdraco
homeassistant/components/lyric/* @timmo001
homeassistant/components/mastodon/* @fabaff
homeassistant/components/matrix/* @tinloaf
homeassistant/components/mcp23017/* @jardiamj

View File

@ -0,0 +1,200 @@
"""The Honeywell Lyric integration."""
import asyncio
from datetime import timedelta
import logging
from typing import Any, Dict, Optional
from aiolyric import Lyric
from aiolyric.objects.device import LyricDevice
from aiolyric.objects.location import LyricLocation
import async_timeout
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import (
aiohttp_client,
config_entry_oauth2_flow,
config_validation as cv,
device_registry as dr,
)
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
from .api import ConfigEntryLyricClient, LyricLocalOAuth2Implementation
from .config_flow import OAuth2FlowHandler
from .const import DOMAIN, LYRIC_EXCEPTIONS, OAUTH2_AUTHORIZE, OAUTH2_TOKEN
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_CLIENT_SECRET): cv.string,
}
)
},
extra=vol.ALLOW_EXTRA,
)
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["climate"]
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Honeywell Lyric component."""
hass.data[DOMAIN] = {}
if DOMAIN not in config:
return True
hass.data[DOMAIN][CONF_CLIENT_ID] = config[DOMAIN][CONF_CLIENT_ID]
OAuth2FlowHandler.async_register_implementation(
hass,
LyricLocalOAuth2Implementation(
hass,
DOMAIN,
config[DOMAIN][CONF_CLIENT_ID],
config[DOMAIN][CONF_CLIENT_SECRET],
OAUTH2_AUTHORIZE,
OAUTH2_TOKEN,
),
)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Honeywell Lyric from a config entry."""
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
)
session = aiohttp_client.async_get_clientsession(hass)
oauth_session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
client = ConfigEntryLyricClient(session, oauth_session)
client_id = hass.data[DOMAIN][CONF_CLIENT_ID]
lyric: Lyric = Lyric(client, client_id)
async def async_update_data() -> Lyric:
"""Fetch data from Lyric."""
try:
async with async_timeout.timeout(60):
await lyric.get_locations()
return lyric
except (*LYRIC_EXCEPTIONS, TimeoutError) as exception:
raise UpdateFailed(exception) from exception
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
# Name of the data. For logging purposes.
name="lyric_coordinator",
update_method=async_update_data,
# Polling interval. Will only be polled if there are subscribers.
update_interval=timedelta(seconds=120),
)
hass.data[DOMAIN][entry.entry_id] = coordinator
# Fetch initial data so we have data when entities subscribe
await coordinator.async_refresh()
if not coordinator.last_update_success:
raise ConfigEntryNotReady
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
class LyricEntity(CoordinatorEntity):
"""Defines a base Honeywell Lyric entity."""
def __init__(
self,
coordinator: DataUpdateCoordinator,
location: LyricLocation,
device: LyricDevice,
key: str,
name: str,
icon: Optional[str],
) -> None:
"""Initialize the Honeywell Lyric entity."""
super().__init__(coordinator)
self._key = key
self._name = name
self._icon = icon
self._location = location
self._mac_id = device.macID
self._device_name = device.name
self._device_model = device.deviceModel
self._update_thermostat = coordinator.data.update_thermostat
@property
def unique_id(self) -> str:
"""Return the unique ID for this entity."""
return self._key
@property
def name(self) -> str:
"""Return the name of the entity."""
return self._name
@property
def icon(self) -> str:
"""Return the mdi icon of the entity."""
return self._icon
@property
def location(self) -> LyricLocation:
"""Get the Lyric Location."""
return self.coordinator.data.locations_dict[self._location.locationID]
@property
def device(self) -> LyricDevice:
"""Get the Lyric Device."""
return self.location.devices_dict[self._mac_id]
class LyricDeviceEntity(LyricEntity):
"""Defines a Honeywell Lyric device entity."""
@property
def device_info(self) -> Dict[str, Any]:
"""Return device information about this Honeywell Lyric instance."""
return {
"connections": {(dr.CONNECTION_NETWORK_MAC, self._mac_id)},
"manufacturer": "Honeywell",
"model": self._device_model,
"name": self._device_name,
}

View File

@ -0,0 +1,55 @@
"""API for Honeywell Lyric bound to Home Assistant OAuth."""
import logging
from typing import cast
from aiohttp import BasicAuth, ClientSession
from aiolyric.client import LyricClient
from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers.aiohttp_client import async_get_clientsession
_LOGGER = logging.getLogger(__name__)
class ConfigEntryLyricClient(LyricClient):
"""Provide Honeywell Lyric authentication tied to an OAuth2 based config entry."""
def __init__(
self,
websession: ClientSession,
oauth_session: config_entry_oauth2_flow.OAuth2Session,
):
"""Initialize Honeywell Lyric auth."""
super().__init__(websession)
self._oauth_session = oauth_session
async def async_get_access_token(self):
"""Return a valid access token."""
if not self._oauth_session.valid_token:
await self._oauth_session.async_ensure_token_valid()
return self._oauth_session.token["access_token"]
class LyricLocalOAuth2Implementation(
config_entry_oauth2_flow.LocalOAuth2Implementation
):
"""Lyric Local OAuth2 implementation."""
async def _token_request(self, data: dict) -> dict:
"""Make a token request."""
session = async_get_clientsession(self.hass)
data["client_id"] = self.client_id
if self.client_secret is not None:
data["client_secret"] = self.client_secret
headers = {
"Authorization": BasicAuth(self.client_id, self.client_secret).encode(),
"Content-Type": "application/x-www-form-urlencoded",
}
resp = await session.post(self.token_url, headers=headers, data=data)
resp.raise_for_status()
return cast(dict, await resp.json())

View File

@ -0,0 +1,280 @@
"""Support for Honeywell Lyric climate platform."""
import logging
from time import gmtime, strftime, time
from typing import List, Optional
from aiolyric.objects.device import LyricDevice
from aiolyric.objects.location import LyricLocation
import voluptuous as vol
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
HVAC_MODE_COOL,
HVAC_MODE_HEAT,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF,
SUPPORT_PRESET_MODE,
SUPPORT_TARGET_TEMPERATURE,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_platform
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import LyricDeviceEntity
from .const import (
DOMAIN,
LYRIC_EXCEPTIONS,
PRESET_HOLD_UNTIL,
PRESET_NO_HOLD,
PRESET_PERMANENT_HOLD,
PRESET_TEMPORARY_HOLD,
PRESET_VACATION_HOLD,
)
_LOGGER = logging.getLogger(__name__)
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
LYRIC_HVAC_MODE_OFF = "Off"
LYRIC_HVAC_MODE_HEAT = "Heat"
LYRIC_HVAC_MODE_COOL = "Cool"
LYRIC_HVAC_MODE_HEAT_COOL = "Auto"
LYRIC_HVAC_MODES = {
HVAC_MODE_OFF: LYRIC_HVAC_MODE_OFF,
HVAC_MODE_HEAT: LYRIC_HVAC_MODE_HEAT,
HVAC_MODE_COOL: LYRIC_HVAC_MODE_COOL,
HVAC_MODE_HEAT_COOL: LYRIC_HVAC_MODE_HEAT_COOL,
}
HVAC_MODES = {
LYRIC_HVAC_MODE_OFF: HVAC_MODE_OFF,
LYRIC_HVAC_MODE_HEAT: HVAC_MODE_HEAT,
LYRIC_HVAC_MODE_COOL: HVAC_MODE_COOL,
LYRIC_HVAC_MODE_HEAT_COOL: HVAC_MODE_HEAT_COOL,
}
SERVICE_HOLD_TIME = "set_hold_time"
ATTR_TIME_PERIOD = "time_period"
SCHEMA_HOLD_TIME = {
vol.Required(ATTR_TIME_PERIOD, default="01:00:00"): vol.All(
cv.time_period,
cv.positive_timedelta,
lambda td: strftime("%H:%M:%S", gmtime(time() + td.total_seconds())),
)
}
async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
) -> None:
"""Set up the Honeywell Lyric climate platform based on a config entry."""
coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
entities = []
for location in coordinator.data.locations:
for device in location.devices:
entities.append(LyricClimate(hass, coordinator, location, device))
async_add_entities(entities, True)
platform = entity_platform.current_platform.get()
platform.async_register_entity_service(
SERVICE_HOLD_TIME,
SCHEMA_HOLD_TIME,
"async_set_hold_time",
)
class LyricClimate(LyricDeviceEntity, ClimateEntity):
"""Defines a Honeywell Lyric climate entity."""
def __init__(
self,
hass: HomeAssistantType,
coordinator: DataUpdateCoordinator,
location: LyricLocation,
device: LyricDevice,
) -> None:
"""Initialize Honeywell Lyric climate entity."""
self._temperature_unit = hass.config.units.temperature_unit
# Setup supported hvac modes
self._hvac_modes = [HVAC_MODE_OFF]
# Add supported lyric thermostat features
if LYRIC_HVAC_MODE_HEAT in device.allowedModes:
self._hvac_modes.append(HVAC_MODE_HEAT)
if LYRIC_HVAC_MODE_COOL in device.allowedModes:
self._hvac_modes.append(HVAC_MODE_COOL)
if (
LYRIC_HVAC_MODE_HEAT in device.allowedModes
and LYRIC_HVAC_MODE_COOL in device.allowedModes
):
self._hvac_modes.append(HVAC_MODE_HEAT_COOL)
super().__init__(
coordinator,
location,
device,
f"{device.macID}_thermostat",
device.name,
None,
)
@property
def supported_features(self) -> int:
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement."""
return self._temperature_unit
@property
def current_temperature(self) -> Optional[float]:
"""Return the current temperature."""
return self.device.indoorTemperature
@property
def hvac_mode(self) -> str:
"""Return the hvac mode."""
return HVAC_MODES[self.device.changeableValues.mode]
@property
def hvac_modes(self) -> List[str]:
"""List of available hvac modes."""
return self._hvac_modes
@property
def target_temperature(self) -> Optional[float]:
"""Return the temperature we try to reach."""
device: LyricDevice = self.device
if not device.hasDualSetpointStatus:
return device.changeableValues.heatSetpoint
@property
def target_temperature_low(self) -> Optional[float]:
"""Return the upper bound temperature we try to reach."""
device: LyricDevice = self.device
if device.hasDualSetpointStatus:
return device.changeableValues.coolSetpoint
@property
def target_temperature_high(self) -> Optional[float]:
"""Return the upper bound temperature we try to reach."""
device: LyricDevice = self.device
if device.hasDualSetpointStatus:
return device.changeableValues.heatSetpoint
@property
def preset_mode(self) -> Optional[str]:
"""Return current preset mode."""
return self.device.changeableValues.thermostatSetpointStatus
@property
def preset_modes(self) -> Optional[List[str]]:
"""Return preset modes."""
return [
PRESET_NO_HOLD,
PRESET_HOLD_UNTIL,
PRESET_PERMANENT_HOLD,
PRESET_TEMPORARY_HOLD,
PRESET_VACATION_HOLD,
]
@property
def min_temp(self) -> float:
"""Identify min_temp in Lyric API or defaults if not available."""
device: LyricDevice = self.device
if LYRIC_HVAC_MODE_COOL in device.allowedModes:
return device.minCoolSetpoint
return device.minHeatSetpoint
@property
def max_temp(self) -> float:
"""Identify max_temp in Lyric API or defaults if not available."""
device: LyricDevice = self.device
if LYRIC_HVAC_MODE_HEAT in device.allowedModes:
return device.maxHeatSetpoint
return device.maxCoolSetpoint
async def async_set_temperature(self, **kwargs) -> None:
"""Set new target temperature."""
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
device: LyricDevice = self.device
if device.hasDualSetpointStatus:
if target_temp_low is not None and target_temp_high is not None:
temp = (target_temp_low, target_temp_high)
else:
raise HomeAssistantError(
"Could not find target_temp_low and/or target_temp_high in arguments"
)
else:
temp = kwargs.get(ATTR_TEMPERATURE)
_LOGGER.debug("Set temperature: %s", temp)
try:
await self._update_thermostat(self.location, device, heatSetpoint=temp)
except LYRIC_EXCEPTIONS as exception:
_LOGGER.error(exception)
await self.coordinator.async_refresh()
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
"""Set hvac mode."""
_LOGGER.debug("Set hvac mode: %s", hvac_mode)
try:
await self._update_thermostat(
self.location, self.device, mode=LYRIC_HVAC_MODES[hvac_mode]
)
except LYRIC_EXCEPTIONS as exception:
_LOGGER.error(exception)
await self.coordinator.async_refresh()
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set preset (PermanentHold, HoldUntil, NoHold, VacationHold) mode."""
_LOGGER.debug("Set preset mode: %s", preset_mode)
try:
await self._update_thermostat(
self.location, self.device, thermostatSetpointStatus=preset_mode
)
except LYRIC_EXCEPTIONS as exception:
_LOGGER.error(exception)
await self.coordinator.async_refresh()
async def async_set_preset_period(self, period: str) -> None:
"""Set preset period (time)."""
try:
await self._update_thermostat(
self.location, self.device, nextPeriodTime=period
)
except LYRIC_EXCEPTIONS as exception:
_LOGGER.error(exception)
await self.coordinator.async_refresh()
async def async_set_hold_time(self, time_period: str) -> None:
"""Set the time to hold until."""
_LOGGER.debug("set_hold_time: %s", time_period)
try:
await self._update_thermostat(
self.location,
self.device,
thermostatSetpointStatus=PRESET_HOLD_UNTIL,
nextPeriodTime=time_period,
)
except LYRIC_EXCEPTIONS as exception:
_LOGGER.error(exception)
await self.coordinator.async_refresh()

View File

@ -0,0 +1,23 @@
"""Config flow for Honeywell Lyric."""
import logging
from homeassistant import config_entries
from homeassistant.helpers import config_entry_oauth2_flow
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class OAuth2FlowHandler(
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
):
"""Config flow to handle Honeywell Lyric OAuth2 authentication."""
DOMAIN = DOMAIN
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH
@property
def logger(self) -> logging.Logger:
"""Return logger."""
return logging.getLogger(__name__)

View File

@ -0,0 +1,20 @@
"""Constants for the Honeywell Lyric integration."""
from aiohttp.client_exceptions import ClientResponseError
from aiolyric.exceptions import LyricAuthenticationException, LyricException
DOMAIN = "lyric"
OAUTH2_AUTHORIZE = "https://api.honeywell.com/oauth2/authorize"
OAUTH2_TOKEN = "https://api.honeywell.com/oauth2/token"
PRESET_NO_HOLD = "NoHold"
PRESET_TEMPORARY_HOLD = "TemporaryHold"
PRESET_HOLD_UNTIL = "HoldUntil"
PRESET_PERMANENT_HOLD = "PermanentHold"
PRESET_VACATION_HOLD = "VacationHold"
LYRIC_EXCEPTIONS = (
LyricAuthenticationException,
LyricException,
ClientResponseError,
)

View File

@ -0,0 +1,24 @@
{
"domain": "lyric",
"name": "Honeywell Lyric",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/lyric",
"dependencies": ["http"],
"requirements": ["aiolyric==1.0.5"],
"codeowners": ["@timmo001"],
"quality_scale": "silver",
"dhcp": [
{
"hostname": "lyric-*",
"macaddress": "48A2E6"
},
{
"hostname": "lyric-*",
"macaddress": "B82CA0"
},
{
"hostname": "lyric-*",
"macaddress": "00D02D"
}
]
}

View File

@ -0,0 +1,9 @@
set_hold_time:
description: "Sets the time to hold until"
fields:
entity_id:
description: Name(s) of entities to change
example: "climate.thermostat"
time_period:
description: Time to hold until
example: "01:00:00"

View File

@ -0,0 +1,16 @@
{
"config": {
"step": {
"pick_implementation": {
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
}
},
"abort": {
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]"
},
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"
}
}
}

View File

@ -0,0 +1,17 @@
{
"config": {
"abort": {
"authorize_url_timeout": "Timeout generating authorize URL.",
"missing_configuration": "The component is not configured. Please follow the documentation."
},
"create_entry": {
"default": "Successfully authenticated"
},
"step": {
"pick_implementation": {
"title": "Pick Authentication Method"
}
}
},
"title": "Honeywell Lyric"
}

View File

@ -121,6 +121,7 @@ FLOWS = [
"logi_circle",
"luftdaten",
"lutron_caseta",
"lyric",
"mailgun",
"melcloud",
"met",

View File

@ -41,6 +41,21 @@ DHCP = [
"hostname": "flume-gw-*",
"macaddress": "B4E62D*"
},
{
"domain": "lyric",
"hostname": "lyric-*",
"macaddress": "48A2E6"
},
{
"domain": "lyric",
"hostname": "lyric-*",
"macaddress": "B82CA0"
},
{
"domain": "lyric",
"hostname": "lyric-*",
"macaddress": "00D02D"
},
{
"domain": "nest",
"macaddress": "18B430*"

View File

@ -199,6 +199,9 @@ aiolifx_effects==0.2.2
# homeassistant.components.lutron_caseta
aiolip==1.0.1
# homeassistant.components.lyric
aiolyric==1.0.5
# homeassistant.components.keyboard_remote
aionotify==0.2.0

View File

@ -118,6 +118,9 @@ aiokafka==0.6.0
# homeassistant.components.lutron_caseta
aiolip==1.0.1
# homeassistant.components.lyric
aiolyric==1.0.5
# homeassistant.components.notion
aionotion==1.1.0

View File

@ -0,0 +1 @@
"""Tests for the Honeywell Lyric integration."""

View File

@ -0,0 +1,128 @@
"""Test the Honeywell Lyric config flow."""
import asyncio
from unittest.mock import patch
import pytest
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.http import CONF_BASE_URL, DOMAIN as DOMAIN_HTTP
from homeassistant.components.lyric import config_flow
from homeassistant.components.lyric.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
from homeassistant.helpers import config_entry_oauth2_flow
CLIENT_ID = "1234"
CLIENT_SECRET = "5678"
@pytest.fixture()
async def mock_impl(hass):
"""Mock implementation."""
await setup.async_setup_component(hass, "http", {})
impl = config_entry_oauth2_flow.LocalOAuth2Implementation(
hass,
DOMAIN,
CLIENT_ID,
CLIENT_SECRET,
OAUTH2_AUTHORIZE,
OAUTH2_TOKEN,
)
config_flow.OAuth2FlowHandler.async_register_implementation(hass, impl)
return impl
async def test_abort_if_no_configuration(hass):
"""Check flow abort when no configuration."""
flow = config_flow.OAuth2FlowHandler()
flow.hass = hass
result = await flow.async_step_user()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "missing_configuration"
async def test_full_flow(
hass, aiohttp_client, aioclient_mock, current_request_with_host
):
"""Check full flow."""
assert await setup.async_setup_component(
hass,
DOMAIN,
{
DOMAIN: {
CONF_CLIENT_ID: CLIENT_ID,
CONF_CLIENT_SECRET: CLIENT_SECRET,
},
DOMAIN_HTTP: {CONF_BASE_URL: "https://example.com"},
},
)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
state = config_entry_oauth2_flow._encode_jwt(
hass,
{
"flow_id": result["flow_id"],
"redirect_uri": "https://example.com/auth/external/callback",
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP
assert result["url"] == (
f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
"&redirect_uri=https://example.com/auth/external/callback"
f"&state={state}"
)
client = await aiohttp_client(hass.http.app)
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
assert resp.status == 200
assert resp.headers["content-type"] == "text/html; charset=utf-8"
aioclient_mock.post(
OAUTH2_TOKEN,
json={
"refresh_token": "mock-refresh-token",
"access_token": "mock-access-token",
"type": "Bearer",
"expires_in": 60,
},
)
with patch("homeassistant.components.lyric.api.ConfigEntryLyricClient"):
with patch(
"homeassistant.components.lyric.async_setup_entry", return_value=True
) as mock_setup:
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["data"]["auth_implementation"] == DOMAIN
result["data"]["token"].pop("expires_at")
assert result["data"]["token"] == {
"refresh_token": "mock-refresh-token",
"access_token": "mock-access-token",
"type": "Bearer",
"expires_in": 60,
}
assert DOMAIN in hass.config.components
entry = hass.config_entries.async_entries(DOMAIN)[0]
assert entry.state == config_entries.ENTRY_STATE_LOADED
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert len(mock_setup.mock_calls) == 1
async def test_abort_if_authorization_timeout(hass, mock_impl):
"""Check Somfy authorization timeout."""
flow = config_flow.OAuth2FlowHandler()
flow.hass = hass
with patch.object(
mock_impl, "async_generate_authorize_url", side_effect=asyncio.TimeoutError
):
result = await flow.async_step_user()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "authorize_url_timeout"