mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Add a one touch pairing config flow for lutron caseta (#45136)
Co-authored-by: Robert Svensson <Kane610@users.noreply.github.com>
This commit is contained in:
parent
03fb73c0ae
commit
431b143eec
@ -257,7 +257,7 @@ homeassistant/components/luci/* @mzdrale
|
||||
homeassistant/components/luftdaten/* @fabaff
|
||||
homeassistant/components/lupusec/* @majuss
|
||||
homeassistant/components/lutron/* @JonGilmore
|
||||
homeassistant/components/lutron_caseta/* @swails
|
||||
homeassistant/components/lutron_caseta/* @swails @bdraco
|
||||
homeassistant/components/mastodon/* @fabaff
|
||||
homeassistant/components/matrix/* @tinloaf
|
||||
homeassistant/components/mcp23017/* @jardiamj
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""Component for interacting with a Lutron Caseta system."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from pylutron_caseta.smartbridge import Smartbridge
|
||||
@ -39,23 +40,24 @@ LUTRON_CASETA_COMPONENTS = ["light", "switch", "cover", "scene", "fan", "binary_
|
||||
async def async_setup(hass, base_config):
|
||||
"""Set up the Lutron component."""
|
||||
|
||||
bridge_configs = base_config[DOMAIN]
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
for config in bridge_configs:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
# extract the config keys one-by-one just to be explicit
|
||||
data={
|
||||
CONF_HOST: config[CONF_HOST],
|
||||
CONF_KEYFILE: config[CONF_KEYFILE],
|
||||
CONF_CERTFILE: config[CONF_CERTFILE],
|
||||
CONF_CA_CERTS: config[CONF_CA_CERTS],
|
||||
},
|
||||
if DOMAIN in base_config:
|
||||
bridge_configs = base_config[DOMAIN]
|
||||
for config in bridge_configs:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
# extract the config keys one-by-one just to be explicit
|
||||
data={
|
||||
CONF_HOST: config[CONF_HOST],
|
||||
CONF_KEYFILE: config[CONF_KEYFILE],
|
||||
CONF_CERTFILE: config[CONF_CERTFILE],
|
||||
CONF_CA_CERTS: config[CONF_CA_CERTS],
|
||||
},
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@ -91,6 +93,26 @@ async def async_setup_entry(hass, config_entry):
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass, config_entry):
|
||||
"""Unload the bridge bridge from a config entry."""
|
||||
|
||||
hass.data[DOMAIN][config_entry.entry_id].close()
|
||||
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(config_entry, component)
|
||||
for component in LUTRON_CASETA_COMPONENTS
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
class LutronCasetaDevice(Entity):
|
||||
"""Common base class for all Lutron Caseta devices."""
|
||||
|
||||
|
@ -1,10 +1,16 @@
|
||||
"""Config flow for Lutron Caseta."""
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
from pylutron_caseta.pairing import PAIR_CA, PAIR_CERT, PAIR_KEY, async_pair
|
||||
from pylutron_caseta.smartbridge import Smartbridge
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.components.zeroconf import ATTR_HOSTNAME
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
|
||||
from . import DOMAIN # pylint: disable=unused-import
|
||||
from .const import (
|
||||
@ -17,10 +23,22 @@ from .const import (
|
||||
STEP_IMPORT_FAILED,
|
||||
)
|
||||
|
||||
HOSTNAME = "hostname"
|
||||
|
||||
|
||||
FILE_MAPPING = {
|
||||
PAIR_KEY: CONF_KEYFILE,
|
||||
PAIR_CERT: CONF_CERTFILE,
|
||||
PAIR_CA: CONF_CA_CERTS,
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ENTRY_DEFAULT_TITLE = "Caséta bridge"
|
||||
|
||||
DATA_SCHEMA_USER = vol.Schema({vol.Required(CONF_HOST): str})
|
||||
TLS_ASSET_TEMPLATE = "lutron_caseta-{}-{}.pem"
|
||||
|
||||
|
||||
class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle Lutron Caseta config flow."""
|
||||
@ -31,6 +49,111 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
def __init__(self):
|
||||
"""Initialize a Lutron Caseta flow."""
|
||||
self.data = {}
|
||||
self.lutron_id = None
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initialized by the user."""
|
||||
if user_input is not None:
|
||||
self.data[CONF_HOST] = user_input[CONF_HOST]
|
||||
return await self.async_step_link()
|
||||
|
||||
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA_USER)
|
||||
|
||||
async def async_step_zeroconf(self, discovery_info):
|
||||
"""Handle a flow initialized by zeroconf discovery."""
|
||||
hostname = discovery_info[ATTR_HOSTNAME]
|
||||
if hostname is None or not hostname.startswith("lutron-"):
|
||||
return self.async_abort(reason="not_lutron_device")
|
||||
|
||||
self.lutron_id = hostname.split("-")[1].replace(".local.", "")
|
||||
|
||||
await self.async_set_unique_id(self.lutron_id)
|
||||
host = discovery_info[CONF_HOST]
|
||||
self._abort_if_unique_id_configured({CONF_HOST: host})
|
||||
|
||||
self.data[CONF_HOST] = host
|
||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||
self.context["title_placeholders"] = {
|
||||
CONF_NAME: self.bridge_id,
|
||||
CONF_HOST: host,
|
||||
}
|
||||
return await self.async_step_link()
|
||||
|
||||
async_step_homekit = async_step_zeroconf
|
||||
|
||||
async def async_step_link(self, user_input=None):
|
||||
"""Handle pairing with the hub."""
|
||||
errors = {}
|
||||
# Abort if existing entry with matching host exists.
|
||||
if self._async_data_host_is_already_configured():
|
||||
return self.async_abort(reason=ABORT_REASON_ALREADY_CONFIGURED)
|
||||
|
||||
self._configure_tls_assets()
|
||||
|
||||
if user_input is not None:
|
||||
if (
|
||||
await self.hass.async_add_executor_job(self._tls_assets_exist)
|
||||
and await self.async_validate_connectable_bridge_config()
|
||||
):
|
||||
# If we previous paired and the tls assets already exist,
|
||||
# we do not need to go though pairing again.
|
||||
return self.async_create_entry(title=self.bridge_id, data=self.data)
|
||||
|
||||
assets = None
|
||||
try:
|
||||
assets = await async_pair(self.data[CONF_HOST])
|
||||
except (asyncio.TimeoutError, OSError):
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
if not errors:
|
||||
await self.hass.async_add_executor_job(self._write_tls_assets, assets)
|
||||
return self.async_create_entry(title=self.bridge_id, data=self.data)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="link",
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
CONF_NAME: self.bridge_id,
|
||||
CONF_HOST: self.data[CONF_HOST],
|
||||
},
|
||||
)
|
||||
|
||||
@property
|
||||
def bridge_id(self):
|
||||
"""Return the best identifier for the bridge.
|
||||
|
||||
If the bridge was not discovered via zeroconf,
|
||||
we fallback to using the host.
|
||||
"""
|
||||
return self.lutron_id or self.data[CONF_HOST]
|
||||
|
||||
def _write_tls_assets(self, assets):
|
||||
"""Write the tls assets to disk."""
|
||||
for asset_key, conf_key in FILE_MAPPING.items():
|
||||
with open(self.hass.config.path(self.data[conf_key]), "w") as file_handle:
|
||||
file_handle.write(assets[asset_key])
|
||||
|
||||
def _tls_assets_exist(self):
|
||||
"""Check to see if tls assets are already on disk."""
|
||||
for conf_key in FILE_MAPPING.values():
|
||||
if not os.path.exists(self.hass.config.path(self.data[conf_key])):
|
||||
return False
|
||||
return True
|
||||
|
||||
@callback
|
||||
def _configure_tls_assets(self):
|
||||
"""Fill the tls asset locations in self.data."""
|
||||
for asset_key, conf_key in FILE_MAPPING.items():
|
||||
self.data[conf_key] = TLS_ASSET_TEMPLATE.format(self.bridge_id, asset_key)
|
||||
|
||||
@callback
|
||||
def _async_data_host_is_already_configured(self):
|
||||
"""Check to see if the host is already configured."""
|
||||
return any(
|
||||
self.data[CONF_HOST] == entry.data[CONF_HOST]
|
||||
for entry in self._async_current_entries()
|
||||
if CONF_HOST in entry.data
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_info):
|
||||
"""Import a new Caseta bridge as a config entry.
|
||||
@ -38,15 +161,14 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
This flow is triggered by `async_setup`.
|
||||
"""
|
||||
|
||||
# Abort if existing entry with matching host exists.
|
||||
host = import_info[CONF_HOST]
|
||||
if any(
|
||||
host == entry.data[CONF_HOST] for entry in self._async_current_entries()
|
||||
):
|
||||
return self.async_abort(reason=ABORT_REASON_ALREADY_CONFIGURED)
|
||||
|
||||
# Store the imported config for other steps in this flow to access.
|
||||
self.data[CONF_HOST] = host
|
||||
|
||||
# Abort if existing entry with matching host exists.
|
||||
if self._async_data_host_is_already_configured():
|
||||
return self.async_abort(reason=ABORT_REASON_ALREADY_CONFIGURED)
|
||||
|
||||
self.data[CONF_KEYFILE] = import_info[CONF_KEYFILE]
|
||||
self.data[CONF_CERTFILE] = import_info[CONF_CERTFILE]
|
||||
self.data[CONF_CA_CERTS] = import_info[CONF_CA_CERTS]
|
||||
@ -68,6 +190,9 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_import_failed(self, user_input=None):
|
||||
"""Make failed import surfaced to user."""
|
||||
|
||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||
self.context["title_placeholders"] = {CONF_NAME: self.data[CONF_HOST]}
|
||||
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id=STEP_IMPORT_FAILED,
|
||||
|
@ -3,9 +3,12 @@
|
||||
"name": "Lutron Caséta",
|
||||
"documentation": "https://www.home-assistant.io/integrations/lutron_caseta",
|
||||
"requirements": [
|
||||
"pylutron-caseta==0.7.2"
|
||||
"pylutron-caseta==0.8.0"
|
||||
],
|
||||
"codeowners": [
|
||||
"@swails"
|
||||
]
|
||||
}
|
||||
"config_flow": true,
|
||||
"zeroconf": ["_leap._tcp.local."],
|
||||
"homekit": {
|
||||
"models": ["Smart Bridge"]
|
||||
},
|
||||
"codeowners": ["@swails", "@bdraco"]
|
||||
}
|
||||
|
@ -1,15 +1,28 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "Lutron Caséta {name} ({host})",
|
||||
"step": {
|
||||
"import_failed": {
|
||||
"title": "Failed to import Caséta bridge configuration.",
|
||||
"description": "Couldn’t setup bridge (host: {host}) imported from configuration.yaml."
|
||||
},
|
||||
"user": {
|
||||
"title": "Automaticlly connect to the bridge",
|
||||
"description": "Enter the ip address of the device.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
}
|
||||
},
|
||||
"link": {
|
||||
"title": "Pair with the bridge",
|
||||
"description": "To pair with {name} ({host}), after submitting this form, press the black button on the back of the bridge."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"abort": {
|
||||
"not_lutron_device": "Discovered device is not a Lutron device",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
}
|
||||
|
@ -2,15 +2,28 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured",
|
||||
"cannot_connect": "Failed to connect"
|
||||
"cannot_connect": "Failed to connect",
|
||||
"not_lutron_device": "Discovered device is not a Lutron device"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect"
|
||||
},
|
||||
"flow_title": "Lutron Cas\u00e9ta {name} ({host})",
|
||||
"step": {
|
||||
"import_failed": {
|
||||
"description": "Couldn\u2019t setup bridge (host: {host}) imported from configuration.yaml.",
|
||||
"title": "Failed to import Cas\u00e9ta bridge configuration."
|
||||
},
|
||||
"link": {
|
||||
"description": "To pair with {name} ({host}), after submitting this form, press the black button on the back of the bridge.",
|
||||
"title": "Pair with the bridge"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host"
|
||||
},
|
||||
"description": "Enter the ip address of the device.",
|
||||
"title": "Automaticlly connect to the bridge"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -117,6 +117,7 @@ FLOWS = [
|
||||
"locative",
|
||||
"logi_circle",
|
||||
"luftdaten",
|
||||
"lutron_caseta",
|
||||
"mailgun",
|
||||
"melcloud",
|
||||
"met",
|
||||
|
@ -96,6 +96,11 @@ ZEROCONF = {
|
||||
"name": "gateway*"
|
||||
}
|
||||
],
|
||||
"_leap._tcp.local.": [
|
||||
{
|
||||
"domain": "lutron_caseta"
|
||||
}
|
||||
],
|
||||
"_mediaremotetv._tcp.local.": [
|
||||
{
|
||||
"domain": "apple_tv"
|
||||
@ -179,6 +184,7 @@ HOMEKIT = {
|
||||
"PowerView": "hunterdouglas_powerview",
|
||||
"Presence": "netatmo",
|
||||
"Rachio": "rachio",
|
||||
"Smart Bridge": "lutron_caseta",
|
||||
"Socket": "wemo",
|
||||
"TRADFRI": "tradfri",
|
||||
"Welcome": "netatmo",
|
||||
|
@ -1500,7 +1500,7 @@ pylitejet==0.1
|
||||
pyloopenergy==0.2.1
|
||||
|
||||
# homeassistant.components.lutron_caseta
|
||||
pylutron-caseta==0.7.2
|
||||
pylutron-caseta==0.8.0
|
||||
|
||||
# homeassistant.components.lutron
|
||||
pylutron==0.2.5
|
||||
|
@ -761,7 +761,7 @@ pylibrespot-java==0.1.0
|
||||
pylitejet==0.1
|
||||
|
||||
# homeassistant.components.lutron_caseta
|
||||
pylutron-caseta==0.7.2
|
||||
pylutron-caseta==0.8.0
|
||||
|
||||
# homeassistant.components.mailgun
|
||||
pymailgunner==1.4
|
||||
|
@ -1,9 +1,12 @@
|
||||
"""Test the Lutron Caseta config flow."""
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from pylutron_caseta.pairing import PAIR_CA, PAIR_CERT, PAIR_KEY
|
||||
from pylutron_caseta.smartbridge import Smartbridge
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant import config_entries, data_entry_flow, setup
|
||||
from homeassistant.components.lutron_caseta import DOMAIN
|
||||
import homeassistant.components.lutron_caseta.config_flow as CasetaConfigFlow
|
||||
from homeassistant.components.lutron_caseta.const import (
|
||||
@ -13,10 +16,17 @@ from homeassistant.components.lutron_caseta.const import (
|
||||
ERROR_CANNOT_CONNECT,
|
||||
STEP_IMPORT_FAILED,
|
||||
)
|
||||
from homeassistant.components.zeroconf import ATTR_HOSTNAME
|
||||
from homeassistant.const import CONF_HOST
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
MOCK_ASYNC_PAIR_SUCCESS = {
|
||||
PAIR_KEY: "mock_key",
|
||||
PAIR_CERT: "mock_cert",
|
||||
PAIR_CA: "mock_ca",
|
||||
}
|
||||
|
||||
|
||||
class MockBridge:
|
||||
"""Mock Lutron bridge that emulates configured connected status."""
|
||||
@ -158,3 +168,335 @@ async def test_duplicate_bridge_import(hass):
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == CasetaConfigFlow.ABORT_REASON_ALREADY_CONFIGURED
|
||||
assert len(mock_setup_entry.mock_calls) == 0
|
||||
|
||||
|
||||
async def test_already_configured_with_ignored(hass):
|
||||
"""Test ignored entries do not break checking for existing entries."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
config_entry = MockConfigEntry(domain=DOMAIN, data={}, source="ignore")
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
data={
|
||||
CONF_HOST: "1.1.1.1",
|
||||
CONF_KEYFILE: "",
|
||||
CONF_CERTFILE: "",
|
||||
CONF_CA_CERTS: "",
|
||||
},
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
|
||||
|
||||
async def test_form_user(hass, tmpdir):
|
||||
"""Test we get the form and can pair."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
hass.config.config_dir = await hass.async_add_executor_job(
|
||||
tmpdir.mkdir, "tls_assets"
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] is None
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: "1.1.1.1",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result2["type"] == "form"
|
||||
assert result2["step_id"] == "link"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.lutron_caseta.config_flow.async_pair",
|
||||
return_value=MOCK_ASYNC_PAIR_SUCCESS,
|
||||
), patch(
|
||||
"homeassistant.components.lutron_caseta.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.lutron_caseta.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] == "create_entry"
|
||||
assert result3["title"] == "1.1.1.1"
|
||||
assert result3["data"] == {
|
||||
CONF_HOST: "1.1.1.1",
|
||||
CONF_KEYFILE: "lutron_caseta-1.1.1.1-key.pem",
|
||||
CONF_CERTFILE: "lutron_caseta-1.1.1.1-cert.pem",
|
||||
CONF_CA_CERTS: "lutron_caseta-1.1.1.1-ca.pem",
|
||||
}
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_user_pairing_fails(hass, tmpdir):
|
||||
"""Test we get the form and we handle pairing failure."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
hass.config.config_dir = await hass.async_add_executor_job(
|
||||
tmpdir.mkdir, "tls_assets"
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] is None
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: "1.1.1.1",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result2["type"] == "form"
|
||||
assert result2["step_id"] == "link"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.lutron_caseta.config_flow.async_pair",
|
||||
side_effect=asyncio.TimeoutError,
|
||||
), patch(
|
||||
"homeassistant.components.lutron_caseta.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.lutron_caseta.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] == "form"
|
||||
assert result3["errors"] == {"base": "cannot_connect"}
|
||||
assert len(mock_setup.mock_calls) == 0
|
||||
assert len(mock_setup_entry.mock_calls) == 0
|
||||
|
||||
|
||||
async def test_form_user_reuses_existing_assets_when_pairing_again(hass, tmpdir):
|
||||
"""Test the tls assets saved on disk are reused when pairing again."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
hass.config.config_dir = await hass.async_add_executor_job(
|
||||
tmpdir.mkdir, "tls_assets"
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] is None
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: "1.1.1.1",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result2["type"] == "form"
|
||||
assert result2["step_id"] == "link"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.lutron_caseta.config_flow.async_pair",
|
||||
return_value=MOCK_ASYNC_PAIR_SUCCESS,
|
||||
), patch(
|
||||
"homeassistant.components.lutron_caseta.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.lutron_caseta.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] == "create_entry"
|
||||
assert result3["title"] == "1.1.1.1"
|
||||
assert result3["data"] == {
|
||||
CONF_HOST: "1.1.1.1",
|
||||
CONF_KEYFILE: "lutron_caseta-1.1.1.1-key.pem",
|
||||
CONF_CERTFILE: "lutron_caseta-1.1.1.1-cert.pem",
|
||||
CONF_CA_CERTS: "lutron_caseta-1.1.1.1-ca.pem",
|
||||
}
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.lutron_caseta.async_unload_entry", return_value=True
|
||||
) as mock_unload:
|
||||
await hass.config_entries.async_remove(result3["result"].entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_unload.mock_calls) == 1
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] is None
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: "1.1.1.1",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result2["type"] == "form"
|
||||
assert result2["step_id"] == "link"
|
||||
|
||||
with patch.object(Smartbridge, "create_tls") as create_tls, patch(
|
||||
"homeassistant.components.lutron_caseta.async_setup", return_value=True
|
||||
), patch(
|
||||
"homeassistant.components.lutron_caseta.async_setup_entry",
|
||||
return_value=True,
|
||||
):
|
||||
create_tls.return_value = MockBridge(can_connect=True)
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] == "create_entry"
|
||||
assert result3["title"] == "1.1.1.1"
|
||||
assert result3["data"] == {
|
||||
CONF_HOST: "1.1.1.1",
|
||||
CONF_KEYFILE: "lutron_caseta-1.1.1.1-key.pem",
|
||||
CONF_CERTFILE: "lutron_caseta-1.1.1.1-cert.pem",
|
||||
CONF_CA_CERTS: "lutron_caseta-1.1.1.1-ca.pem",
|
||||
}
|
||||
|
||||
|
||||
async def test_zeroconf_host_already_configured(hass, tmpdir):
|
||||
"""Test starting a flow from discovery when the host is already configured."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
hass.config.config_dir = await hass.async_add_executor_job(
|
||||
tmpdir.mkdir, "tls_assets"
|
||||
)
|
||||
|
||||
config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_HOST: "1.1.1.1"})
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
data={
|
||||
CONF_HOST: "1.1.1.1",
|
||||
ATTR_HOSTNAME: "lutron-abc.local.",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_zeroconf_lutron_id_already_configured(hass):
|
||||
"""Test starting a flow from discovery when lutron id already configured."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "4.5.6.7"}, unique_id="abc"
|
||||
)
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
data={
|
||||
CONF_HOST: "1.1.1.1",
|
||||
ATTR_HOSTNAME: "lutron-abc.local.",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "already_configured"
|
||||
assert config_entry.data[CONF_HOST] == "1.1.1.1"
|
||||
|
||||
|
||||
async def test_zeroconf_not_lutron_device(hass):
|
||||
"""Test starting a flow from discovery when it is not a lutron device."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
data={
|
||||
CONF_HOST: "1.1.1.1",
|
||||
ATTR_HOSTNAME: "notlutron-abc.local.",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "not_lutron_device"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"source", (config_entries.SOURCE_ZEROCONF, config_entries.SOURCE_HOMEKIT)
|
||||
)
|
||||
async def test_zeroconf(hass, source, tmpdir):
|
||||
"""Test starting a flow from discovery."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
hass.config.config_dir = await hass.async_add_executor_job(
|
||||
tmpdir.mkdir, "tls_assets"
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": source},
|
||||
data={
|
||||
CONF_HOST: "1.1.1.1",
|
||||
ATTR_HOSTNAME: "lutron-abc.local.",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "link"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.lutron_caseta.config_flow.async_pair",
|
||||
return_value=MOCK_ASYNC_PAIR_SUCCESS,
|
||||
), patch(
|
||||
"homeassistant.components.lutron_caseta.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.lutron_caseta.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == "create_entry"
|
||||
assert result2["title"] == "abc"
|
||||
assert result2["data"] == {
|
||||
CONF_HOST: "1.1.1.1",
|
||||
CONF_KEYFILE: "lutron_caseta-abc-key.pem",
|
||||
CONF_CERTFILE: "lutron_caseta-abc-cert.pem",
|
||||
CONF_CA_CERTS: "lutron_caseta-abc-ca.pem",
|
||||
}
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
Loading…
x
Reference in New Issue
Block a user