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
parent
80a08199f8
commit
be6cf7d3ae
|
@ -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
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
|
@ -0,0 +1,3 @@
|
|||
"""Lutron constants."""
|
||||
|
||||
DOMAIN = "lutron"
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -282,6 +282,7 @@ FLOWS = {
|
|||
"lookin",
|
||||
"loqed",
|
||||
"luftdaten",
|
||||
"lutron",
|
||||
"lutron_caseta",
|
||||
"lyric",
|
||||
"mailgun",
|
||||
|
|
|
@ -3280,7 +3280,7 @@
|
|||
"integrations": {
|
||||
"lutron": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Lutron"
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
"""Test for the lutron integration."""
|
|
@ -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
|
|
@ -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"
|
Loading…
Reference in New Issue