diff --git a/.coveragerc b/.coveragerc index 40daa9ce2307c5..aa7fd218375f29 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1085,7 +1085,10 @@ omit = homeassistant/components/velbus/light.py homeassistant/components/velbus/sensor.py homeassistant/components/velbus/switch.py - homeassistant/components/velux/* + homeassistant/components/velux/__init__.py + homeassistant/components/velux/const.py + homeassistant/components/velux/cover.py + homeassistant/components/velux/scene.py homeassistant/components/venstar/climate.py homeassistant/components/verisure/__init__.py homeassistant/components/verisure/alarm_control_panel.py diff --git a/homeassistant/components/velux/__init__.py b/homeassistant/components/velux/__init__.py index 5c1d8bfd37060a..f1b7959d38ce0d 100644 --- a/homeassistant/components/velux/__init__.py +++ b/homeassistant/components/velux/__init__.py @@ -4,12 +4,13 @@ from pyvlx import PyVLX, PyVLXException import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP -from homeassistant.helpers import discovery +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -DOMAIN = "velux" -DATA_VELUX = "data_velux" +from .const import CONFIG_KEY_MODULE, DOMAIN + PLATFORMS = ["cover", "scene"] _LOGGER = logging.getLogger(__name__) @@ -23,12 +24,29 @@ ) -async def async_setup(hass, config): - """Set up the velux component.""" +async def async_setup(hass: HomeAssistant, config: dict): + """Component setup, run import config flow for each entry in config.""" + if DOMAIN not in config: + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Set up Velux using config flow.""" try: - hass.data[DATA_VELUX] = VeluxModule(hass, config[DOMAIN]) - hass.data[DATA_VELUX].setup() - await hass.data[DATA_VELUX].async_start() + veluxModule = VeluxModule(hass, config_entry) + veluxModule.setup() + await veluxModule.async_start() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][config_entry.entry_id] = {CONFIG_KEY_MODULE: veluxModule} except PyVLXException as ex: _LOGGER.exception("Can't connect to velux interface: %s", ex) @@ -36,7 +54,7 @@ async def async_setup(hass, config): for platform in PLATFORMS: hass.async_create_task( - discovery.async_load_platform(hass, platform, DOMAIN, {}, config) + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) return True @@ -44,7 +62,7 @@ async def async_setup(hass, config): class VeluxModule: """Abstraction for velux component.""" - def __init__(self, hass, domain_config): + def __init__(self, hass: HomeAssistant, domain_config): """Initialize for velux component.""" self.pyvlx = None self._hass = hass @@ -62,8 +80,8 @@ async def async_reboot_gateway(service_call): await self.pyvlx.reboot_gateway() self._hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) - host = self._domain_config.get(CONF_HOST) - password = self._domain_config.get(CONF_PASSWORD) + host = self._domain_config.data[CONF_HOST] + password = self._domain_config.data[CONF_PASSWORD] self.pyvlx = PyVLX(host=host, password=password) self._hass.services.async_register( diff --git a/homeassistant/components/velux/config_flow.py b/homeassistant/components/velux/config_flow.py new file mode 100644 index 00000000000000..990b3c195544fd --- /dev/null +++ b/homeassistant/components/velux/config_flow.py @@ -0,0 +1,60 @@ +"""Config flow for velux integration.""" +import logging + +from pyvlx import PyVLX, PyVLXException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PASSWORD + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + {vol.Required(CONF_HOST): str, vol.Required(CONF_PASSWORD): str} +) + + +class VeluxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for velux.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + host = user_input[CONF_HOST] + password = user_input[CONF_PASSWORD] + + await self.async_set_unique_id(host) + self._abort_if_unique_id_configured() + + pyvlx = PyVLX(host=host, password=password) + await pyvlx.connect() + + await pyvlx.disconnect() + + return self.async_create_entry( + title=host, + data=user_input, + ) + except PyVLXException as ex: + _LOGGER.exception("Unable to connect to Velux gateway: %s", ex) + errors["base"] = "invalid_auth" + except Exception as ex: # pylint: disable=broad-except + _LOGGER.exception("Unable to connect to Velux gateway: %s", ex) + errors["base"] = "unknown" + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + async def async_step_import(self, import_config): + """Import config from configuration.yaml.""" + self._async_abort_entries_match({CONF_HOST: import_config[CONF_HOST]}) + + return await self.async_step_user(import_config) diff --git a/homeassistant/components/velux/const.py b/homeassistant/components/velux/const.py new file mode 100644 index 00000000000000..34bc032621b48c --- /dev/null +++ b/homeassistant/components/velux/const.py @@ -0,0 +1,4 @@ +"""Const for Velux.""" + +DOMAIN = "velux" +CONFIG_KEY_MODULE = "velux_module" diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index 187c0d3617860d..977040e0983f43 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -21,15 +21,21 @@ SUPPORT_STOP_TILT, CoverEntity, ) -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback -from . import DATA_VELUX +from .const import CONFIG_KEY_MODULE, DOMAIN -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities, +) -> None: """Set up cover(s) for Velux platform.""" entities = [] - for node in hass.data[DATA_VELUX].pyvlx.nodes: + veluxModule = hass.data[DOMAIN][entry.entry_id][CONFIG_KEY_MODULE] + for node in veluxModule.pyvlx.nodes: if isinstance(node, OpeningDevice): entities.append(VeluxCover(node)) async_add_entities(entities) diff --git a/homeassistant/components/velux/manifest.json b/homeassistant/components/velux/manifest.json index 43be9b424a8464..85c14ede098abe 100644 --- a/homeassistant/components/velux/manifest.json +++ b/homeassistant/components/velux/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/velux", "requirements": ["pyvlx==0.2.18"], "codeowners": ["@Julius2342"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "config_flow": true } diff --git a/homeassistant/components/velux/scene.py b/homeassistant/components/velux/scene.py index 96ff0558fff791..3bf65cfc049d1b 100644 --- a/homeassistant/components/velux/scene.py +++ b/homeassistant/components/velux/scene.py @@ -2,13 +2,21 @@ from typing import Any from homeassistant.components.scene import Scene +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant -from . import _LOGGER, DATA_VELUX +from . import _LOGGER +from .const import CONFIG_KEY_MODULE, DOMAIN -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities, +) -> None: """Set up the scenes for Velux platform.""" - entities = [VeluxScene(scene) for scene in hass.data[DATA_VELUX].pyvlx.scenes] + veluxModule = hass.data[DOMAIN][entry.entry_id][CONFIG_KEY_MODULE] + entities = [VeluxScene(scene) for scene in veluxModule.pyvlx.scenes] async_add_entities(entities) diff --git a/homeassistant/components/velux/strings.json b/homeassistant/components/velux/strings.json new file mode 100644 index 00000000000000..590e85a819ca05 --- /dev/null +++ b/homeassistant/components/velux/strings.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "user": { + "title": "Setup your Velux KLF 200", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 151b95a8f20309..68a6d1c0a93f90 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -256,6 +256,7 @@ "upcloud", "upnp", "velbus", + "velux", "vera", "verisure", "vesync", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0419b9b3c5f97c..ddddd39f592db0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1018,6 +1018,9 @@ pyvesync==1.3.1 # homeassistant.components.vizio pyvizio==0.1.57 +# homeassistant.components.velux +pyvlx==0.2.18 + # homeassistant.components.volumio pyvolumio==0.1.3 diff --git a/tests/components/velux/__init__.py b/tests/components/velux/__init__.py new file mode 100644 index 00000000000000..00477bf4e67198 --- /dev/null +++ b/tests/components/velux/__init__.py @@ -0,0 +1 @@ +"""Tests for the Velux Component.""" diff --git a/tests/components/velux/test_config_flow.py b/tests/components/velux/test_config_flow.py new file mode 100644 index 00000000000000..615fb1990b886f --- /dev/null +++ b/tests/components/velux/test_config_flow.py @@ -0,0 +1,39 @@ +"""Test the Velux config flow.""" +from unittest.mock import patch + +from pyvlx import PyVLXException + +from homeassistant import config_entries, setup +from homeassistant.components.velux import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD + +CONFIG = {DOMAIN: {CONF_HOST: "192.168.0.20", CONF_PASSWORD: "password"}} + + +async def test_form(hass): + """Test we get the form.""" + return + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + +async def test_gateway_connect_exception(hass): + """Test a error message is displayed when connection to KLF gateway fails.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "pyvlx.PyVLX.connect", + side_effect=PyVLXException("Login to KLF 200 failed, check credentials"), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG[DOMAIN] + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"}