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:
parent
0f15d2bf19
commit
f0d5ae2fec
13 changed files with 722 additions and 57 deletions
|
@ -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/*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
129
homeassistant/components/yale_smart_alarm/config_flow.py
Normal file
129
homeassistant/components/yale_smart_alarm/config_flow.py
Normal 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,
|
||||
)
|
39
homeassistant/components/yale_smart_alarm/const.py
Normal file
39
homeassistant/components/yale_smart_alarm/const.py
Normal 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,
|
||||
}
|
139
homeassistant/components/yale_smart_alarm/coordinator.py
Normal file
139
homeassistant/components/yale_smart_alarm/coordinator.py
Normal 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,
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
|
|
28
homeassistant/components/yale_smart_alarm/strings.json
Normal file
28
homeassistant/components/yale_smart_alarm/strings.json
Normal 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%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -300,6 +300,7 @@ FLOWS = [
|
|||
"xbox",
|
||||
"xiaomi_aqara",
|
||||
"xiaomi_miio",
|
||||
"yale_smart_alarm",
|
||||
"yamaha_musiccast",
|
||||
"yeelight",
|
||||
"zerproc",
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
1
tests/components/yale_smart_alarm/__init__.py
Normal file
1
tests/components/yale_smart_alarm/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Tests for the Yale Smart Living integration."""
|
224
tests/components/yale_smart_alarm/test_config_flow.py
Normal file
224
tests/components/yale_smart_alarm/test_config_flow.py
Normal 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"}
|
Loading…
Add table
Reference in a new issue