Add yale_smart_alarm config flow and coordinator (#50850)

* config flow and coordinator

* comply with pylint

* Remove pylint errors

* Update test coverage yale smart alarm

* Update test config_flow

* Fix test already configured

* Second try test already configured

* Fixes config flow and tests

* Conform pylint errors coordinator

* Fix various review remarks

* Correct entity unique id

* Fix unique id and migrate entries

* Remove lock code

* Remove code from test

* Expand code coverage config flow test

* Add more constants

* Add test new requirements

* Minor corrections

* Resolve conflict alarm schema

* Change logger

* Changed from review

* Fix isort error

* Fix flake error

* Ignore mypy errors

* Corrections from PR review no 2

* Corrections from PR review no 3

* Added tests and fix pylint error

* Corrections from PR review no 4

* Corrections from PR review no 5

* Corrections from PR review no 6

* Corrections from PR review no 6_2

* Corrections from PR review no 7

* Corrections from PR review no 8

* Minor last changes for PR

* Update homeassistant/components/yale_smart_alarm/coordinator.py

Co-authored-by: Raman Gupta <7243222+raman325@users.noreply.github.com>
This commit is contained in:
G Johansson 2021-07-24 19:55:43 +02:00 committed by GitHub
parent 0f15d2bf19
commit f0d5ae2fec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 722 additions and 57 deletions

View File

@ -1214,7 +1214,10 @@ omit =
homeassistant/components/xiaomi_tv/media_player.py
homeassistant/components/xmpp/notify.py
homeassistant/components/xs1/*
homeassistant/components/yale_smart_alarm/__init__.py
homeassistant/components/yale_smart_alarm/alarm_control_panel.py
homeassistant/components/yale_smart_alarm/const.py
homeassistant/components/yale_smart_alarm/coordinator.py
homeassistant/components/yamaha_musiccast/__init__.py
homeassistant/components/yamaha_musiccast/media_player.py
homeassistant/components/yandex_transport/*

View File

@ -1 +1,46 @@
"""The yale_smart_alarm component."""
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from .const import DOMAIN, LOGGER, PLATFORMS
from .coordinator import YaleDataUpdateCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Yale from a config entry."""
hass.data.setdefault(DOMAIN, {})
title = entry.title
coordinator = YaleDataUpdateCoordinator(hass, entry=entry)
if not await hass.async_add_executor_job(coordinator.get_updates):
raise ConfigEntryAuthFailed
await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = {
"coordinator": coordinator,
}
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
LOGGER.debug("Loaded entry for %s", title)
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)
title = entry.title
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
LOGGER.debug("Unloaded entry for %s", title)
return unload_ok
return False

View File

@ -1,14 +1,7 @@
"""Component for interacting with the Yale Smart Alarm System API."""
import logging
"""Support for Yale Alarm."""
from __future__ import annotations
import voluptuous as vol
from yalesmartalarmclient.client import (
YALE_STATE_ARM_FULL,
YALE_STATE_ARM_PARTIAL,
YALE_STATE_DISARM,
AuthenticationError,
YaleSmartAlarmClient,
)
from homeassistant.components.alarm_control_panel import (
PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA,
@ -18,23 +11,38 @@ from homeassistant.components.alarm_control_panel.const import (
SUPPORT_ALARM_ARM_AWAY,
SUPPORT_ALARM_ARM_HOME,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
ATTR_IDENTIFIERS,
ATTR_MANUFACTURER,
ATTR_MODEL,
ATTR_NAME,
CONF_NAME,
CONF_PASSWORD,
CONF_USERNAME,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
)
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import (
AddEntitiesCallback,
ConfigType,
DiscoveryInfoType,
)
from homeassistant.helpers.update_coordinator import CoordinatorEntity
CONF_AREA_ID = "area_id"
DEFAULT_NAME = "Yale Smart Alarm"
DEFAULT_AREA_ID = "1"
_LOGGER = logging.getLogger(__name__)
from .const import (
CONF_AREA_ID,
COORDINATOR,
DEFAULT_AREA_ID,
DEFAULT_NAME,
DOMAIN,
LOGGER,
MANUFACTURER,
MODEL,
STATE_MAP,
)
from .coordinator import YaleDataUpdateCoordinator
PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(
{
@ -46,66 +54,82 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(
)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the alarm platform."""
name = config[CONF_NAME]
username = config[CONF_USERNAME]
password = config[CONF_PASSWORD]
area_id = config[CONF_AREA_ID]
try:
client = YaleSmartAlarmClient(username, password, area_id)
except AuthenticationError:
_LOGGER.error("Authentication failed. Check credentials")
return
add_entities([YaleAlarmDevice(name, client)], True)
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Import Yale configuration from YAML."""
LOGGER.warning(
"Loading Yale Alarm via platform setup is deprecated; Please remove it from your configuration"
)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=config,
)
)
class YaleAlarmDevice(AlarmControlPanelEntity):
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the alarm entry."""
async_add_entities(
[YaleAlarmDevice(coordinator=hass.data[DOMAIN][entry.entry_id][COORDINATOR])]
)
class YaleAlarmDevice(CoordinatorEntity, AlarmControlPanelEntity):
"""Represent a Yale Smart Alarm."""
def __init__(self, name, client):
"""Initialize the Yale Alarm Device."""
self._name = name
self._client = client
self._state = None
coordinator: YaleDataUpdateCoordinator
self._state_map = {
YALE_STATE_DISARM: STATE_ALARM_DISARMED,
YALE_STATE_ARM_PARTIAL: STATE_ALARM_ARMED_HOME,
YALE_STATE_ARM_FULL: STATE_ALARM_ARMED_AWAY,
}
_attr_name: str = coordinator.entry.data[CONF_NAME]
_attr_unique_id: str = coordinator.entry.entry_id
_identifier: str = coordinator.entry.data[CONF_USERNAME]
@property
def name(self):
"""Return the name of the device."""
return self._name
def device_info(self) -> DeviceInfo:
"""Return device information about this entity."""
return {
ATTR_NAME: str(self.name),
ATTR_MANUFACTURER: MANUFACTURER,
ATTR_MODEL: MODEL,
ATTR_IDENTIFIERS: {(DOMAIN, self._identifier)},
}
@property
def state(self):
"""Return the state of the device."""
return self._state
return STATE_MAP.get(self.coordinator.data["alarm"])
@property
def available(self):
"""Return if entity is available."""
return STATE_MAP.get(self.coordinator.data["alarm"]) is not None
@property
def code_arm_required(self):
"""Whether the code is required for arm actions."""
return False
@property
def supported_features(self) -> int:
"""Return the list of supported features."""
return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY
def update(self):
"""Return the state of the device."""
armed_status = self._client.get_armed_status()
self._state = self._state_map.get(armed_status)
def alarm_disarm(self, code=None):
"""Send disarm command."""
self._client.disarm()
self.coordinator.yale.disarm()
def alarm_arm_home(self, code=None):
"""Send arm home command."""
self._client.arm_partial()
self.coordinator.yale.arm_partial()
def alarm_arm_away(self, code=None):
"""Send arm away command."""
self._client.arm_full()
self.coordinator.yale.arm_full()

View File

@ -0,0 +1,129 @@
"""Adds config flow for Yale Smart Alarm integration."""
from __future__ import annotations
import voluptuous as vol
from yalesmartalarmclient.client import AuthenticationError, YaleSmartAlarmClient
from homeassistant import config_entries
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME
import homeassistant.helpers.config_validation as cv
from .const import CONF_AREA_ID, DEFAULT_AREA_ID, DEFAULT_NAME, DOMAIN, LOGGER
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_AREA_ID, default=DEFAULT_AREA_ID): cv.string,
}
)
DATA_SCHEMA_AUTH = vol.Schema(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
}
)
class YaleConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Yale integration."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
entry: config_entries.ConfigEntry
async def async_step_import(self, config: dict):
"""Import a configuration from config.yaml."""
self.context.update(
{"title_placeholders": {CONF_NAME: f"YAML import {DOMAIN}"}}
)
return await self.async_step_user(user_input=config)
async def async_step_reauth(self, user_input=None):
"""Handle initiation of re-authentication with Yale."""
self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(self, user_input=None):
"""Dialog that informs the user that reauth is required."""
errors = {}
if user_input is not None:
username = user_input[CONF_USERNAME]
password = user_input[CONF_PASSWORD]
try:
await self.hass.async_add_executor_job(
YaleSmartAlarmClient, username, password
)
except AuthenticationError as error:
LOGGER.error("Authentication failed. Check credentials %s", error)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=DATA_SCHEMA,
errors={"base": "invalid_auth"},
)
existing_entry = await self.async_set_unique_id(username)
if existing_entry:
self.hass.config_entries.async_update_entry(
existing_entry,
data={
**self.entry.data,
CONF_USERNAME: username,
CONF_PASSWORD: password,
},
)
await self.hass.config_entries.async_reload(existing_entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
data_schema=DATA_SCHEMA_AUTH,
errors=errors,
)
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
if user_input is not None:
username = user_input[CONF_USERNAME]
password = user_input[CONF_PASSWORD]
name = user_input.get(CONF_NAME, DEFAULT_NAME)
area = user_input.get(CONF_AREA_ID, DEFAULT_AREA_ID)
try:
await self.hass.async_add_executor_job(
YaleSmartAlarmClient, username, password
)
except AuthenticationError as error:
LOGGER.error("Authentication failed. Check credentials %s", error)
return self.async_show_form(
step_id="user",
data_schema=DATA_SCHEMA,
errors={"base": "invalid_auth"},
)
await self.async_set_unique_id(username)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=username,
data={
CONF_USERNAME: username,
CONF_PASSWORD: password,
CONF_NAME: name,
CONF_AREA_ID: area,
},
)
return self.async_show_form(
step_id="user",
data_schema=DATA_SCHEMA,
errors=errors,
)

View File

@ -0,0 +1,39 @@
"""Yale integration constants."""
import logging
from yalesmartalarmclient.client import (
YALE_STATE_ARM_FULL,
YALE_STATE_ARM_PARTIAL,
YALE_STATE_DISARM,
)
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
)
CONF_AREA_ID = "area_id"
DEFAULT_NAME = "Yale Smart Alarm"
DEFAULT_AREA_ID = "1"
MANUFACTURER = "Yale"
MODEL = "main"
DOMAIN = "yale_smart_alarm"
COORDINATOR = "coordinator"
DEFAULT_SCAN_INTERVAL = 15
LOGGER = logging.getLogger(__name__)
ATTR_ONLINE = "online"
ATTR_STATUS = "status"
PLATFORMS = ["alarm_control_panel"]
STATE_MAP = {
YALE_STATE_DISARM: STATE_ALARM_DISARMED,
YALE_STATE_ARM_PARTIAL: STATE_ALARM_ARMED_HOME,
YALE_STATE_ARM_FULL: STATE_ALARM_ARMED_AWAY,
}

View File

@ -0,0 +1,139 @@
"""DataUpdateCoordinator for the Yale integration."""
from __future__ import annotations
from datetime import timedelta
from yalesmartalarmclient.client import AuthenticationError, YaleSmartAlarmClient
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER
class YaleDataUpdateCoordinator(DataUpdateCoordinator):
"""A Yale Data Update Coordinator."""
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Initialize the Yale hub."""
self.entry = entry
self.yale: YaleSmartAlarmClient | None = None
super().__init__(
hass,
LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
)
async def _async_update_data(self) -> dict:
"""Fetch data from Yale."""
updates = await self.hass.async_add_executor_job(self.get_updates)
locks = []
door_windows = []
for device in updates["cycle"]["data"]["device_status"]:
state = device["status1"]
if device["type"] == "device_type.door_lock":
lock_status_str = device["minigw_lock_status"]
lock_status = int(str(lock_status_str or 0), 16)
closed = (lock_status & 16) == 16
locked = (lock_status & 1) == 1
if not lock_status and "device_status.lock" in state:
device["_state"] = "locked"
locks.append(device)
continue
if not lock_status and "device_status.unlock" in state:
device["_state"] = "unlocked"
locks.append(device)
continue
if (
lock_status
and (
"device_status.lock" in state or "device_status.unlock" in state
)
and closed
and locked
):
device["_state"] = "locked"
locks.append(device)
continue
if (
lock_status
and (
"device_status.lock" in state or "device_status.unlock" in state
)
and closed
and not locked
):
device["_state"] = "unlocked"
locks.append(device)
continue
if (
lock_status
and (
"device_status.lock" in state or "device_status.unlock" in state
)
and not closed
):
device["_state"] = "unlocked"
locks.append(device)
continue
device["_state"] = "unavailable"
locks.append(device)
continue
if device["type"] == "device_type.door_contact":
if "device_status.dc_close" in state:
device["_state"] = "closed"
door_windows.append(device)
continue
if "device_status.dc_open" in state:
device["_state"] = "open"
door_windows.append(device)
continue
device["_state"] = "unavailable"
door_windows.append(device)
continue
return {
"alarm": updates["arm_status"],
"locks": locks,
"door_windows": door_windows,
"status": updates["status"],
"online": updates["online"],
}
def get_updates(self) -> dict:
"""Fetch data from Yale."""
if self.yale is None:
self.yale = YaleSmartAlarmClient(
self.entry.data[CONF_USERNAME], self.entry.data[CONF_PASSWORD]
)
try:
arm_status = self.yale.get_armed_status()
cycle = self.yale.get_cycle()
status = self.yale.get_status()
online = self.yale.get_online()
except AuthenticationError as error:
LOGGER.error("Authentication failed. Check credentials %s", error)
self.hass.async_create_task(
self.hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_REAUTH, "entry_id": self.entry.entry_id},
data=self.entry.data,
)
)
raise UpdateFailed from error
return {
"arm_status": arm_status,
"cycle": cycle,
"status": status,
"online": online,
}

View File

@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/yale_smart_alarm",
"requirements": ["yalesmartalarmclient==0.3.3"],
"codeowners": ["@gjohansson-ST"],
"config_flow": true,
"iot_class": "cloud_polling"
}

View File

@ -0,0 +1,28 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
},
"error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"step": {
"user": {
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]",
"area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]"
}
},
"reauth_confirm": {
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"name": "[%key:component::yale_smart_alarm::config::step::user::data::name%]",
"area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]"
}
}
}
}
}

View File

@ -0,0 +1,28 @@
{
"config": {
"abort": {
"already_configured": "Connection already configured for this account"
},
"error": {
"invalid_auth": "Authentication error"
},
"step": {
"user": {
"data": {
"username": "Username",
"password": "Password",
"name": "Name of alarm",
"area_id": "Area ID"
}
},
"reauth_confirm": {
"data": {
"username": "Usernamn",
"password": "Password",
"name": "Name of alarm",
"area_id": "Area ID"
}
}
}
}
}

View File

@ -300,6 +300,7 @@ FLOWS = [
"xbox",
"xiaomi_aqara",
"xiaomi_miio",
"yale_smart_alarm",
"yamaha_musiccast",
"yeelight",
"zerproc",

View File

@ -1324,6 +1324,9 @@ xknx==0.18.8
# homeassistant.components.zestimate
xmltodict==0.12.0
# homeassistant.components.yale_smart_alarm
yalesmartalarmclient==0.3.3
# homeassistant.components.august
yalexs==1.1.12

View File

@ -0,0 +1 @@
"""Tests for the Yale Smart Living integration."""

View File

@ -0,0 +1,224 @@
"""Test the Yale Smart Living config flow."""
from __future__ import annotations
from unittest.mock import patch
import pytest
from yalesmartalarmclient.client import AuthenticationError
from homeassistant import config_entries, setup
from homeassistant.components.yale_smart_alarm.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM
from tests.common import MockConfigEntry
async def test_form(hass: HomeAssistant) -> None:
"""Test we get the form."""
await setup.async_setup_component(hass, "persistent_notification", {})
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.yale_smart_alarm.config_flow.YaleSmartAlarmClient",
), patch(
"homeassistant.components.yale_smart_alarm.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",
"name": "Yale Smart Alarm",
"area_id": "1",
},
)
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",
"name": "Yale Smart Alarm",
"area_id": "1",
}
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.yale_smart_alarm.config_flow.YaleSmartAlarmClient",
side_effect=AuthenticationError,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"username": "test-username",
"password": "test-password",
"name": "Yale Smart Alarm",
"area_id": "1",
},
)
await hass.async_block_till_done()
assert result2["type"] == "form"
assert result2["errors"] == {"base": "invalid_auth"}
@pytest.mark.parametrize(
"input,output",
[
(
{
"username": "test-username",
"password": "test-password",
"name": "Yale Smart Alarm",
"area_id": "1",
},
{
"username": "test-username",
"password": "test-password",
"name": "Yale Smart Alarm",
"area_id": "1",
},
),
(
{
"username": "test-username",
"password": "test-password",
},
{
"username": "test-username",
"password": "test-password",
"name": "Yale Smart Alarm",
"area_id": "1",
},
),
],
)
async def test_import_flow_success(hass, input: dict[str, str], output: dict[str, str]):
"""Test a successful import of yaml."""
with patch(
"homeassistant.components.yale_smart_alarm.config_flow.YaleSmartAlarmClient",
), patch(
"homeassistant.components.yale_smart_alarm.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=input,
)
await hass.async_block_till_done()
assert result2["type"] == "create_entry"
assert result2["title"] == "test-username"
assert result2["data"] == output
assert len(mock_setup_entry.mock_calls) == 1
async def test_reauth_flow(hass: HomeAssistant) -> None:
"""Test a reauthentication flow."""
entry = MockConfigEntry(
domain=DOMAIN,
unique_id="test-username",
data={
"username": "test-username",
"password": "test-password",
"name": "Yale Smart Alarm",
"area_id": "1",
},
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"unique_id": entry.unique_id,
"entry_id": entry.entry_id,
},
data=entry.data,
)
assert result["step_id"] == "reauth_confirm"
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] == {}
with patch(
"homeassistant.components.yale_smart_alarm.config_flow.YaleSmartAlarmClient",
) as mock_yale, patch(
"homeassistant.components.yale_smart_alarm.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": "new-test-password",
},
)
await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_ABORT
assert result2["reason"] == "reauth_successful"
assert entry.data == {
"username": "test-username",
"password": "new-test-password",
"name": "Yale Smart Alarm",
"area_id": "1",
}
assert len(mock_yale.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_reauth_flow_invalid_login(hass: HomeAssistant) -> None:
"""Test a reauthentication flow."""
entry = MockConfigEntry(
domain=DOMAIN,
unique_id="test-username",
data={
"username": "test-username",
"password": "test-password",
},
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"unique_id": entry.unique_id,
"entry_id": entry.entry_id,
},
data=entry.data,
)
with patch(
"homeassistant.components.yale_smart_alarm.config_flow.YaleSmartAlarmClient",
side_effect=AuthenticationError,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"username": "test-username",
"password": "wrong-password",
},
)
await hass.async_block_till_done()
assert result2["step_id"] == "reauth_confirm"
assert result2["type"] == "form"
assert result2["errors"] == {"base": "invalid_auth"}