Add Lutron config flow (#98489)

* rough in structure for config_flow

* updated json files

* initial conversion to config_flow

* minor updates

* Update binary_sensor.py

* Update config_flow.py

* Update __init__.py

* updates beased on requested changes

* Update const.py

added doc note for ruff

* updated based on suggestions

* updated load xmldb for efficiency

* updated references

* removed unneeded file

* updated config flow to use GUID from XML DB

* minor update to change logging

* updated based on PR feedback

* reverted change for line 30 based on testing

* corrected user_input

* updated based on latest comments

* exception handling

* added raising of issues for config flow

* updated issues strings

* config flow test shell - needs work

* minor changes

* Update strings.json

* Update config_flow.py

* Update __init__.py

* Create conftest.py

* Update test_config_flow.py

* Update test_config_flow.py

* Update config_flow.py

* Update strings.json

* Update requirements_test_all.txt

* Update strings.json

* Update strings.json

* Update config_flow.py

* Update test_config_flow.py

* Update config_flow.py

* Create test_init.py

* Update __init__.py

* Delete tests/components/lutron/test_init.py

* Update strings.json

* updated import parts and tested

* updated strings to improve user feedback

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
pull/107257/head
wilburCforce 2024-01-05 09:39:14 -06:00 committed by GitHub
parent 80a08199f8
commit be6cf7d3ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 549 additions and 85 deletions

View File

@ -748,6 +748,7 @@ build.json @home-assistant/supervisor
/tests/components/luftdaten/ @fabaff @frenck
/homeassistant/components/lupusec/ @majuss
/homeassistant/components/lutron/ @cdheiser
/tests/components/lutron/ @cdheiser
/homeassistant/components/lutron_caseta/ @swails @bdraco @danaues
/tests/components/lutron_caseta/ @swails @bdraco @danaues
/homeassistant/components/lyric/ @timmo001

View File

@ -4,6 +4,8 @@ import logging
from pylutron import Button, Lutron
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ID,
CONF_HOST,
@ -11,14 +13,15 @@ from homeassistant.const import (
CONF_USERNAME,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import discovery
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify
DOMAIN = "lutron"
from .const import DOMAIN
PLATFORMS = [
Platform.LIGHT,
@ -53,8 +56,59 @@ CONFIG_SCHEMA = vol.Schema(
)
def setup(hass: HomeAssistant, base_config: ConfigType) -> bool:
async def _async_import(hass: HomeAssistant, base_config: ConfigType) -> None:
"""Import a config entry from configuration.yaml."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=base_config[DOMAIN],
)
if (
result["type"] == FlowResultType.CREATE_ENTRY
or result["reason"] == "single_instance_allowed"
):
async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_yaml_{DOMAIN}",
breaks_in_ha_version="2024.7.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Lutron",
},
)
return
async_create_issue(
hass,
DOMAIN,
f"deprecated_yaml_import_issue_{result['reason']}",
breaks_in_ha_version="2024.7.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key=f"deprecated_yaml_import_issue_{result['reason']}",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Lutron",
},
)
async def async_setup(hass: HomeAssistant, base_config: ConfigType) -> bool:
"""Set up the Lutron component."""
if DOMAIN in base_config:
hass.async_create_task(_async_import(hass, base_config))
return True
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up the Lutron integration."""
hass.data.setdefault(DOMAIN, {})
hass.data[LUTRON_BUTTONS] = []
hass.data[LUTRON_CONTROLLER] = None
hass.data[LUTRON_DEVICES] = {
@ -64,19 +118,25 @@ def setup(hass: HomeAssistant, base_config: ConfigType) -> bool:
"scene": [],
"binary_sensor": [],
}
host = config_entry.data[CONF_HOST]
uid = config_entry.data[CONF_USERNAME]
pwd = config_entry.data[CONF_PASSWORD]
config = base_config[DOMAIN]
hass.data[LUTRON_CONTROLLER] = Lutron(
config[CONF_HOST], config[CONF_USERNAME], config[CONF_PASSWORD]
)
def _load_db() -> bool:
hass.data[LUTRON_CONTROLLER].load_xml_db()
return True
hass.data[LUTRON_CONTROLLER].load_xml_db()
hass.data[LUTRON_CONTROLLER] = Lutron(host, uid, pwd)
await hass.async_add_executor_job(_load_db)
hass.data[LUTRON_CONTROLLER].connect()
_LOGGER.info("Connected to main repeater at %s", config[CONF_HOST])
_LOGGER.info("Connected to main repeater at %s", host)
# Sort our devices into types
_LOGGER.debug("Start adding devices")
for area in hass.data[LUTRON_CONTROLLER].areas:
_LOGGER.debug("Working on area %s", area.name)
for output in area.outputs:
_LOGGER.debug("Working on output %s", output.type)
if output.type == "SYSTEM_SHADE":
hass.data[LUTRON_DEVICES]["cover"].append((area.name, output))
elif output.is_dimmable:
@ -108,18 +168,22 @@ def setup(hass: HomeAssistant, base_config: ConfigType) -> bool:
hass.data[LUTRON_DEVICES]["binary_sensor"].append(
(area.name, area.occupancy_group)
)
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
for platform in PLATFORMS:
discovery.load_platform(hass, platform, DOMAIN, {}, base_config)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Clean up resources and entities associated with the integration."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
class LutronDevice(Entity):
"""Representation of a Lutron device entity."""
_attr_should_poll = False
def __init__(self, area_name, lutron_device, controller):
def __init__(self, area_name, lutron_device, controller) -> None:
"""Initialize the device."""
self._lutron_device = lutron_device
self._controller = controller
@ -155,7 +219,7 @@ class LutronButton:
represented as an entity; it simply fires events.
"""
def __init__(self, hass, area_name, keypad, button):
def __init__(self, hass: HomeAssistant, area_name, keypad, button) -> None:
"""Register callback for activity on the button."""
name = f"{keypad.name}: {button.name}"
if button.name == "Unknown Button":

View File

@ -1,34 +1,42 @@
"""Support for Lutron Powr Savr occupancy sensors."""
from __future__ import annotations
from collections.abc import Mapping
import logging
from typing import Any
from pylutron import OccupancyGroup
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.typing import DiscoveryInfoType
from . import LUTRON_CONTROLLER, LUTRON_DEVICES, LutronDevice
_LOGGER = logging.getLogger(__name__)
def setup_platform(
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Lutron occupancy sensors."""
if discovery_info is None:
return
devs = []
for area_name, device in hass.data[LUTRON_DEVICES]["binary_sensor"]:
dev = LutronOccupancySensor(area_name, device, hass.data[LUTRON_CONTROLLER])
devs.append(dev)
"""Set up the Lutron binary_sensor platform.
add_entities(devs)
Adds occupancy groups from the Main Repeater associated with the
config_entry as binary_sensor entities.
"""
entities = []
for area_name, device in hass.data[LUTRON_DEVICES]["binary_sensor"]:
entity = LutronOccupancySensor(area_name, device, hass.data[LUTRON_CONTROLLER])
entities.append(entity)
async_add_entities(entities, True)
class LutronOccupancySensor(LutronDevice, BinarySensorEntity):
@ -42,13 +50,13 @@ class LutronOccupancySensor(LutronDevice, BinarySensorEntity):
_attr_device_class = BinarySensorDeviceClass.OCCUPANCY
@property
def is_on(self):
def is_on(self) -> bool:
"""Return true if the binary sensor is on."""
# Error cases will end up treated as unoccupied.
return self._lutron_device.state == OccupancyGroup.State.OCCUPIED
@property
def name(self):
def name(self) -> str:
"""Return the name of the device."""
# The default LutronDevice naming would create 'Kitchen Occ Kitchen',
# but since there can only be one OccupancyGroup per area we go
@ -56,6 +64,6 @@ class LutronOccupancySensor(LutronDevice, BinarySensorEntity):
return f"{self._area_name} Occupancy"
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Return the state attributes."""
return {"lutron_integration_id": self._lutron_device.id}

View File

@ -0,0 +1,107 @@
"""Config flow to configure the Lutron integration."""
from __future__ import annotations
import logging
from typing import Any
from urllib.error import HTTPError
from pylutron import Lutron
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.data_entry_flow import FlowResult
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class LutronConfigFlow(ConfigFlow, domain=DOMAIN):
"""User prompt for Main Repeater configuration information."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""First step in the config flow."""
# Check if a configuration entry already exists
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
errors = {}
if user_input is not None:
ip_address = user_input[CONF_HOST]
main_repeater = Lutron(
ip_address,
user_input.get(CONF_USERNAME),
user_input.get(CONF_PASSWORD),
)
try:
await self.hass.async_add_executor_job(main_repeater.load_xml_db)
except HTTPError:
_LOGGER.exception("Http error")
errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unknown error")
errors["base"] = "unknown"
else:
guid = main_repeater.guid
if len(guid) <= 10:
errors["base"] = "cannot_connect"
if not errors:
await self.async_set_unique_id(guid)
self._abort_if_unique_id_configured()
return self.async_create_entry(title="Lutron", data=user_input)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_USERNAME, default="lutron"): str,
vol.Required(CONF_PASSWORD, default="integration"): str,
}
),
errors=errors,
)
async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult:
"""Attempt to import the existing configuration."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
main_repeater = Lutron(
import_config[CONF_HOST],
import_config[CONF_USERNAME],
import_config[CONF_PASSWORD],
)
def _load_db() -> None:
main_repeater.load_xml_db()
try:
await self.hass.async_add_executor_job(_load_db)
except HTTPError:
_LOGGER.exception("Http error")
return self.async_abort(reason="cannot_connect")
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unknown error")
return self.async_abort(reason="unknown")
guid = main_repeater.guid
if len(guid) <= 10:
return self.async_abort(reason="cannot_connect")
_LOGGER.debug("Main Repeater GUID: %s", main_repeater.guid)
await self.async_set_unique_id(guid)
self._abort_if_unique_id_configured()
return self.async_create_entry(title="Lutron", data=import_config)

View File

@ -0,0 +1,3 @@
"""Lutron constants."""
DOMAIN = "lutron"

View File

@ -1,6 +1,7 @@
"""Support for Lutron shades."""
from __future__ import annotations
from collections.abc import Mapping
import logging
from typing import Any
@ -9,28 +10,30 @@ from homeassistant.components.cover import (
CoverEntity,
CoverEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import LUTRON_CONTROLLER, LUTRON_DEVICES, LutronDevice
_LOGGER = logging.getLogger(__name__)
def setup_platform(
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Lutron shades."""
devs = []
for area_name, device in hass.data[LUTRON_DEVICES]["cover"]:
dev = LutronCover(area_name, device, hass.data[LUTRON_CONTROLLER])
devs.append(dev)
"""Set up the Lutron cover platform.
add_entities(devs, True)
Adds shades from the Main Repeater associated with the config_entry as
cover entities.
"""
entities = []
for area_name, device in hass.data[LUTRON_DEVICES]["cover"]:
entity = LutronCover(area_name, device, hass.data[LUTRON_CONTROLLER])
entities.append(entity)
async_add_entities(entities, True)
class LutronCover(LutronDevice, CoverEntity):
@ -73,6 +76,6 @@ class LutronCover(LutronDevice, CoverEntity):
_LOGGER.debug("Lutron ID: %d updated to %f", self._lutron_device.id, level)
@property
def extra_state_attributes(self) -> dict[str, Any]:
def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Return the state attributes."""
return {"lutron_integration_id": self._lutron_device.id}

View File

@ -1,29 +1,32 @@
"""Support for Lutron lights."""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import LUTRON_CONTROLLER, LUTRON_DEVICES, LutronDevice
def setup_platform(
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Lutron lights."""
devs = []
for area_name, device in hass.data[LUTRON_DEVICES]["light"]:
dev = LutronLight(area_name, device, hass.data[LUTRON_CONTROLLER])
devs.append(dev)
"""Set up the Lutron light platform.
add_entities(devs, True)
Adds dimmers from the Main Repeater associated with the config_entry as
light entities.
"""
entities = []
for area_name, device in hass.data[LUTRON_DEVICES]["light"]:
entity = LutronLight(area_name, device, hass.data[LUTRON_CONTROLLER])
entities.append(entity)
async_add_entities(entities, True)
def to_lutron_level(level):
@ -42,13 +45,13 @@ class LutronLight(LutronDevice, LightEntity):
_attr_color_mode = ColorMode.BRIGHTNESS
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
def __init__(self, area_name, lutron_device, controller):
def __init__(self, area_name, lutron_device, controller) -> None:
"""Initialize the light."""
self._prev_brightness = None
super().__init__(area_name, lutron_device, controller)
@property
def brightness(self):
def brightness(self) -> int:
"""Return the brightness of the light."""
new_brightness = to_hass_level(self._lutron_device.last_level())
if new_brightness != 0:
@ -71,12 +74,12 @@ class LutronLight(LutronDevice, LightEntity):
self._lutron_device.level = 0
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Return the state attributes."""
return {"lutron_integration_id": self._lutron_device.id}
@property
def is_on(self):
def is_on(self) -> bool:
"""Return true if device is on."""
return self._lutron_device.last_level() > 0

View File

@ -2,6 +2,7 @@
"domain": "lutron",
"name": "Lutron",
"codeowners": ["@cdheiser"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/lutron",
"iot_class": "local_polling",
"loggers": ["pylutron"],

View File

@ -4,29 +4,31 @@ from __future__ import annotations
from typing import Any
from homeassistant.components.scene import Scene
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import LUTRON_CONTROLLER, LUTRON_DEVICES, LutronDevice
def setup_platform(
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Lutron scenes."""
devs = []
"""Set up the Lutron scene platform.
Adds scenes from the Main Repeater associated with the config_entry as
scene entities.
"""
entities = []
for scene_data in hass.data[LUTRON_DEVICES]["scene"]:
(area_name, keypad_name, device, led) = scene_data
dev = LutronScene(
entity = LutronScene(
area_name, keypad_name, device, led, hass.data[LUTRON_CONTROLLER]
)
devs.append(dev)
add_entities(devs, True)
entities.append(entity)
async_add_entities(entities, True)
class LutronScene(LutronDevice, Scene):

View File

@ -0,0 +1,35 @@
{
"config": {
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"host": "The hostname or IP address of the Lutron main repeater."
},
"description": "Please enter the main repeater login information",
"title": "Main repeater setup"
}
},
"abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
},
"issues": {
"deprecated_yaml_import_issue_cannot_connect": {
"title": "The Lutron YAML configuration import cannot connect to server",
"description": "Configuring Lutron using YAML is being removed but there was an connection error importing your YAML configuration.\n\nThings you can try:\nMake sure your home assistant can reach the main repeater.\nRestart the main repeater by unplugging it for 60 seconds.\nTry logging into the main repeater at the IP address you specified in a web browser and the same login information.\n\nThen restart Home Assistant to try importing this integration again.\n\nAlternatively, you may remove the Lutron configuration from your YAML configuration entirely, restart Home Assistant, and add the Lutron integration manually."
},
"deprecated_yaml_import_issue_unknown": {
"title": "The Lutron YAML configuration import request failed due to an unknown error",
"description": "Configuring Lutron using YAML is being removed but there was an unknown error while importing your existing configuration.\nSetup will not proceed.\n\nThe specific error can be found in the logs. The most likely cause is a networking error or the Main Repeater is down or has an invalid configuration.\n\nVerify that your Lutron system is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the Lutron configuration from your YAML configuration entirely, restart Home Assistant, and add the Lutron integration manually."
}
}
}

View File

@ -1,29 +1,33 @@
"""Support for Lutron switches."""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import LUTRON_CONTROLLER, LUTRON_DEVICES, LutronDevice
def setup_platform(
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Lutron switches."""
devs = []
"""Set up the Lutron switch platform.
Adds switches from the Main Repeater associated with the config_entry as
switch entities.
"""
entities = []
# Add Lutron Switches
for area_name, device in hass.data[LUTRON_DEVICES]["switch"]:
dev = LutronSwitch(area_name, device, hass.data[LUTRON_CONTROLLER])
devs.append(dev)
entity = LutronSwitch(area_name, device, hass.data[LUTRON_CONTROLLER])
entities.append(entity)
# Add the indicator LEDs for scenes (keypad buttons)
for scene_data in hass.data[LUTRON_DEVICES]["scene"]:
@ -32,15 +36,14 @@ def setup_platform(
led = LutronLed(
area_name, keypad_name, scene, led, hass.data[LUTRON_CONTROLLER]
)
devs.append(led)
add_entities(devs, True)
entities.append(led)
async_add_entities(entities, True)
class LutronSwitch(LutronDevice, SwitchEntity):
"""Representation of a Lutron Switch."""
def __init__(self, area_name, lutron_device, controller):
def __init__(self, area_name, lutron_device, controller) -> None:
"""Initialize the switch."""
self._prev_state = None
super().__init__(area_name, lutron_device, controller)
@ -54,12 +57,12 @@ class LutronSwitch(LutronDevice, SwitchEntity):
self._lutron_device.level = 0
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Return the state attributes."""
return {"lutron_integration_id": self._lutron_device.id}
@property
def is_on(self):
def is_on(self) -> bool:
"""Return true if device is on."""
return self._lutron_device.last_level() > 0
@ -87,7 +90,7 @@ class LutronLed(LutronDevice, SwitchEntity):
self._lutron_device.state = 0
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Return the state attributes."""
return {
"keypad": self._keypad_name,
@ -96,7 +99,7 @@ class LutronLed(LutronDevice, SwitchEntity):
}
@property
def is_on(self):
def is_on(self) -> bool:
"""Return true if device is on."""
return self._lutron_device.last_state

View File

@ -282,6 +282,7 @@ FLOWS = {
"lookin",
"loqed",
"luftdaten",
"lutron",
"lutron_caseta",
"lyric",
"mailgun",

View File

@ -3280,7 +3280,7 @@
"integrations": {
"lutron": {
"integration_type": "hub",
"config_flow": false,
"config_flow": true,
"iot_class": "local_polling",
"name": "Lutron"
},

View File

@ -1450,6 +1450,9 @@ pylitterbot==2023.4.9
# homeassistant.components.lutron_caseta
pylutron-caseta==0.18.3
# homeassistant.components.lutron
pylutron==0.2.8
# homeassistant.components.mailgun
pymailgunner==1.4

View File

@ -0,0 +1 @@
"""Test for the lutron integration."""

View File

@ -0,0 +1,15 @@
"""Provide common Lutron fixtures and mocks."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
import pytest
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.lutron.async_setup_entry", return_value=True
) as mock_setup_entry:
yield mock_setup_entry

View File

@ -0,0 +1,214 @@
"""Test the lutron config flow."""
from unittest.mock import AsyncMock, patch
from urllib.error import HTTPError
import pytest
from homeassistant.components.lutron.const import DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry
MOCK_DATA_STEP = {
CONF_HOST: "127.0.0.1",
CONF_USERNAME: "lutron",
CONF_PASSWORD: "integration",
}
async def test_full_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
"""Test success response."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user"
with patch("homeassistant.components.lutron.config_flow.Lutron.load_xml_db"), patch(
"homeassistant.components.lutron.config_flow.Lutron.guid", "12345678901"
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=MOCK_DATA_STEP,
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["result"].title == "Lutron"
assert result["data"] == MOCK_DATA_STEP
@pytest.mark.parametrize(
("raise_error", "text_error"),
[
(HTTPError("", 404, "", None, {}), "cannot_connect"),
(Exception, "unknown"),
],
)
async def test_flow_failure(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
raise_error: Exception,
text_error: str,
) -> None:
"""Test unknown errors."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user"
with patch(
"homeassistant.components.lutron.config_flow.Lutron.load_xml_db",
side_effect=raise_error,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=MOCK_DATA_STEP,
)
assert result["type"] == FlowResultType.FORM
assert result["errors"] == {"base": text_error}
with patch("homeassistant.components.lutron.config_flow.Lutron.load_xml_db"), patch(
"homeassistant.components.lutron.config_flow.Lutron.guid", "12345678901"
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=MOCK_DATA_STEP,
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["result"].title == "Lutron"
assert result["data"] == MOCK_DATA_STEP
async def test_flow_incorrect_guid(
hass: HomeAssistant, mock_setup_entry: AsyncMock
) -> None:
"""Test configuring flow with incorrect guid."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user"
with patch("homeassistant.components.lutron.config_flow.Lutron.load_xml_db"), patch(
"homeassistant.components.lutron.config_flow.Lutron.guid", "12345"
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=MOCK_DATA_STEP,
)
assert result["type"] == FlowResultType.FORM
assert result["errors"] == {"base": "cannot_connect"}
with patch("homeassistant.components.lutron.config_flow.Lutron.load_xml_db"), patch(
"homeassistant.components.lutron.config_flow.Lutron.guid", "12345678901"
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=MOCK_DATA_STEP,
)
assert result["type"] == FlowResultType.CREATE_ENTRY
async def test_flow_single_instance_allowed(hass: HomeAssistant) -> None:
"""Test we abort user data set when entry is already configured."""
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_DATA_STEP, unique_id="12345678901")
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "single_instance_allowed"
MOCK_DATA_IMPORT = {
CONF_HOST: "127.0.0.1",
CONF_USERNAME: "lutron",
CONF_PASSWORD: "integration",
}
async def test_import(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
) -> None:
"""Test import flow."""
with patch("homeassistant.components.lutron.config_flow.Lutron.load_xml_db"), patch(
"homeassistant.components.lutron.config_flow.Lutron.guid", "12345678901"
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_DATA_IMPORT
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["data"] == MOCK_DATA_IMPORT
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.parametrize(
("raise_error", "reason"),
[
(HTTPError("", 404, "", None, {}), "cannot_connect"),
(Exception, "unknown"),
],
)
async def test_import_flow_failure(
hass: HomeAssistant, raise_error: Exception, reason: str
) -> None:
"""Test handling errors while importing."""
with patch(
"homeassistant.components.lutron.config_flow.Lutron.load_xml_db",
side_effect=raise_error,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_DATA_IMPORT
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == reason
async def test_import_flow_guid_failure(hass: HomeAssistant) -> None:
"""Test handling errors while importing."""
with patch("homeassistant.components.lutron.config_flow.Lutron.load_xml_db"), patch(
"homeassistant.components.lutron.config_flow.Lutron.guid", "123"
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_DATA_IMPORT
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "cannot_connect"
async def test_import_already_configured(hass: HomeAssistant) -> None:
"""Test we abort import when entry is already configured."""
entry = MockConfigEntry(
domain=DOMAIN, data=MOCK_DATA_IMPORT, unique_id="12345678901"
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_DATA_IMPORT
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "single_instance_allowed"