diff --git a/homeassistant/components/velux/__init__.py b/homeassistant/components/velux/__init__.py index bac65c969cff30..131f3fa10b16b5 100644 --- a/homeassistant/components/velux/__init__.py +++ b/homeassistant/components/velux/__init__.py @@ -1,70 +1,66 @@ """Support for VELUX KLF 200 devices.""" import logging -from pyvlx import PyVLX, PyVLXException -import voluptuous as vol +from pyvlx import PyVLX -from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import DOMAIN, PLATFORMS -DOMAIN = "velux" -DATA_VELUX = "data_velux" -SUPPORTED_DOMAINS = ["cover", "scene"] _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - {vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string} + +async def async_setup(hass, config): + """Set up the Velux KLF platform via configuration.yaml.""" + if DOMAIN in config: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": "import"}, data=config[DOMAIN] + ) ) - }, - extra=vol.ALLOW_EXTRA, -) + return True -async def async_setup(hass, config): - """Set up the velux component.""" - try: - hass.data[DATA_VELUX] = VeluxModule(hass, config[DOMAIN]) - hass.data[DATA_VELUX].setup() - await hass.data[DATA_VELUX].async_start() +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up the Velux KLF platforms via Config Flow.""" + _LOGGER.debug("Setting up velux entry: %s", entry.data) + host = entry.data[CONF_HOST] + password = entry.data[CONF_PASSWORD] + gateway = PyVLX(host=host, password=password) - except PyVLXException as ex: - _LOGGER.exception("Can't connect to velux interface: %s", ex) - return False + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = gateway + try: + await gateway.connect() + except (OSError, ConnectionAbortedError) as ex: + _LOGGER.error("Unable to connect to KLF200: %s", str(ex)) + raise ConfigEntryNotReady from ex + await gateway.load_nodes() + await gateway.load_scenes() - for component in SUPPORTED_DOMAINS: + for component in PLATFORMS: hass.async_create_task( - discovery.async_load_platform(hass, component, DOMAIN, {}, config) + hass.config_entries.async_forward_entry_setup(entry, component) ) - return True + async def async_reboot_gateway(service_call): + await gateway.reboot_gateway() -class VeluxModule: - """Abstraction for velux component.""" + hass.services.async_register(DOMAIN, "reboot_gateway", async_reboot_gateway) - def __init__(self, hass, domain_config): - """Initialize for velux component.""" - self.pyvlx = None - self._hass = hass - self._domain_config = domain_config + return True - def setup(self): - """Velux component setup.""" - async def on_hass_stop(event): - """Close connection when hass stops.""" - _LOGGER.debug("Velux interface terminated") - await self.pyvlx.disconnect() +async def async_unload_entry(hass, entry): + """Unloading the Velux platform.""" + gateway = hass.data[DOMAIN][entry.entry_id] + await gateway.reboot_gateway() + await gateway.disconnect() - 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) - self.pyvlx = PyVLX(host=host, password=password) + for component in PLATFORMS: + await hass.config_entries.async_forward_entry_unload(entry, component) - async def async_start(self): - """Start velux component.""" - _LOGGER.debug("Velux interface started") - await self.pyvlx.load_scenes() - await self.pyvlx.load_nodes() + return True diff --git a/homeassistant/components/velux/config_flow.py b/homeassistant/components/velux/config_flow.py new file mode 100644 index 00000000000000..9ded81285258d1 --- /dev/null +++ b/homeassistant/components/velux/config_flow.py @@ -0,0 +1,91 @@ +"""Velux component config flow.""" +# https://developers.home-assistant.io/docs/config_entries_config_flow_handler#defining-your-config-flow +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__) + +RESULT_AUTH_FAILED = "connection_failed" +RESULT_SUCCESS = "success" + + +class VeluxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Velux config flow.""" + + def __init__(self): + """Initialize.""" + self._velux = None + self._host = None + self._password = None + self._hostname = None + self.bridge = None + + def _get_entry(self): + return self.async_create_entry( + title=self._host, + data={CONF_HOST: self._host, CONF_PASSWORD: self._password}, + ) + + async def async_step_import(self, user_input=None): + """Handle configuration by yaml file.""" + return await self.async_step_user(user_input) + + async def async_step_user(self, user_input=None): + """Handle configuration via user input.""" + errors = {} + if user_input is not None: + self._host = user_input[CONF_HOST] + self._password = user_input[CONF_PASSWORD] + await self.async_set_unique_id(self._host) + self._abort_if_unique_id_configured() + self.bridge = PyVLX(host=self._host, password=self._password) + try: + await self.bridge.connect() + await self.bridge.disconnect() + return self._get_entry() + except PyVLXException: + errors["base"] = "invalid_auth" + except OSError: + errors["base"] = "invalid_host" + else: + errors["base"] = "cannot_connect" + + data_schema = vol.Schema( + { + vol.Required(CONF_HOST, default=self._host): str, + vol.Required(CONF_PASSWORD, default=self._password): str, + } + ) + + return self.async_show_form( + step_id="user", data_schema=data_schema, errors=errors + ) + + async def async_step_unignore(self, user_input): + """Rediscover a previously ignored discover.""" + unique_id = user_input["unique_id"] + await self.async_set_unique_id(unique_id) + return await self.async_step_user() + + async def async_step_zeroconf(self, info): + """Handle discovery by zeroconf.""" + if ( + info is None + or not info.get("hostname") + or not info["hostname"].startswith("VELUX_KLF_LAN") + ): + return self.async_abort(reason="no_devices_found") + + self._host = info.get("host") + + await self.async_set_unique_id(self._host) + self._abort_if_unique_id_configured() + + return await self.async_step_user() diff --git a/homeassistant/components/velux/const.py b/homeassistant/components/velux/const.py new file mode 100644 index 00000000000000..6cd668427eab0c --- /dev/null +++ b/homeassistant/components/velux/const.py @@ -0,0 +1,4 @@ +"""Constants for Velux Integration.""" + +DOMAIN = "velux" +PLATFORMS = ["cover", "scene"] diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index e8e210c1e531d4..db2d258bdb9933 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -1,9 +1,12 @@ """Support for Velux covers.""" +import logging + from pyvlx import OpeningDevice, Position from pyvlx.opening_device import Awning, Blind, GarageDoor, Gate, RollerShutter, Window from homeassistant.components.cover import ( ATTR_POSITION, + ATTR_TILT_POSITION, DEVICE_CLASS_AWNING, DEVICE_CLASS_BLIND, DEVICE_CLASS_GARAGE, @@ -11,20 +14,28 @@ DEVICE_CLASS_SHUTTER, DEVICE_CLASS_WINDOW, SUPPORT_CLOSE, + SUPPORT_CLOSE_TILT, SUPPORT_OPEN, + SUPPORT_OPEN_TILT, SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, + SUPPORT_STOP_TILT, CoverEntity, ) from homeassistant.core import callback -from . import DATA_VELUX +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, entry, async_add_entities): """Set up cover(s) for Velux platform.""" entities = [] - for node in hass.data[DATA_VELUX].pyvlx.nodes: + gateway = hass.data[DOMAIN][entry.entry_id] + for node in gateway.nodes: + _LOGGER.debug("Node will be added: %s", node.name) if isinstance(node, OpeningDevice): entities.append(VeluxCover(node)) async_add_entities(entities) @@ -54,7 +65,7 @@ async def async_added_to_hass(self): @property def unique_id(self): """Return the unique ID of this cover.""" - return self.node.serial_number + return self.node.node_id @property def name(self): @@ -69,13 +80,29 @@ def should_poll(self): @property def supported_features(self): """Flag supported features.""" - return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION | SUPPORT_STOP + supported_features = ( + SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION | SUPPORT_STOP + ) + if self.current_cover_tilt_position is not None: + supported_features |= ( + SUPPORT_OPEN_TILT + | SUPPORT_CLOSE_TILT + | SUPPORT_SET_TILT_POSITION + | SUPPORT_STOP_TILT + ) + return supported_features @property def current_cover_position(self): """Return the current position of the cover.""" return 100 - self.node.position.position_percent + @property + def current_cover_tilt_position(self): + """Return the current position of the cover.""" + if isinstance(self.node, Blind): + return 100 - self.node.orientation.position_percent + @property def device_class(self): """Define this cover as either awning, blind, garage, gate, shutter or window.""" @@ -110,7 +137,6 @@ async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" if ATTR_POSITION in kwargs: position_percent = 100 - kwargs[ATTR_POSITION] - await self.node.set_position( Position(position_percent=position_percent), wait_for_completion=False ) @@ -118,3 +144,24 @@ async def async_set_cover_position(self, **kwargs): async def async_stop_cover(self, **kwargs): """Stop the cover.""" await self.node.stop(wait_for_completion=False) + + async def async_close_cover_tilt(self, **kwargs): + """Close cover tilt.""" + await self.node.close_orientation(wait_for_completion=False) + + async def async_open_cover_tilt(self, **kwargs): + """Open cover tilt.""" + await self.node.open_orientation(wait_for_completion=False) + + async def async_stop_cover_tilt(self, **kwargs): + """Stop cover tilt.""" + await self.node.stop_orientation(wait_for_completion=False) + + async def async_set_cover_tilt_position(self, **kwargs): + """Move the cover to a specific position.""" + if ATTR_TILT_POSITION in kwargs: + position_percent = 100 - kwargs[ATTR_TILT_POSITION] + orientation = Position(position_percent=position_percent) + await self.node.set_orientation( + orientation=orientation, wait_for_completion=False + ) diff --git a/homeassistant/components/velux/manifest.json b/homeassistant/components/velux/manifest.json index 73306bca7b5d68..61110bb9741b88 100644 --- a/homeassistant/components/velux/manifest.json +++ b/homeassistant/components/velux/manifest.json @@ -2,6 +2,14 @@ "domain": "velux", "name": "Velux", "documentation": "https://www.home-assistant.io/integrations/velux", - "requirements": ["pyvlx==0.2.16"], - "codeowners": ["@Julius2342"] -} + "codeowners": [ + "@Julius2342" + ], + "requirements": [ + "pyvlx==0.2.18" + ], + "zeroconf": [ + "_http._tcp.local." + ], + "config_flow": true +} \ No newline at end of file diff --git a/homeassistant/components/velux/scene.py b/homeassistant/components/velux/scene.py index 96ff0558fff791..a514c01e3fa3e3 100644 --- a/homeassistant/components/velux/scene.py +++ b/homeassistant/components/velux/scene.py @@ -3,12 +3,14 @@ from homeassistant.components.scene import Scene -from . import _LOGGER, DATA_VELUX +from . import _LOGGER +from .const import DOMAIN -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, entry, async_add_entities): """Set up the scenes for Velux platform.""" - entities = [VeluxScene(scene) for scene in hass.data[DATA_VELUX].pyvlx.scenes] + gateway = hass.data[DOMAIN][entry.entry_id] + entities = [VeluxScene(scene) for scene in gateway.scenes] async_add_entities(entities) @@ -25,6 +27,11 @@ def name(self): """Return the name of the scene.""" return self.scene.name + @property + def unique_id(self): + """Return the unique ID of this scene.""" + return self.scene.scene_id + async def async_activate(self, **kwargs: Any) -> None: """Activate the scene.""" await self.scene.run(wait_for_completion=False) diff --git a/homeassistant/components/velux/services.yaml b/homeassistant/components/velux/services.yaml new file mode 100644 index 00000000000000..2460db0bbb02e9 --- /dev/null +++ b/homeassistant/components/velux/services.yaml @@ -0,0 +1,4 @@ +# Velux Integration services + +reboot_gateway: + description: Reboots the KLF200 Gateway. diff --git a/homeassistant/components/velux/strings.json b/homeassistant/components/velux/strings.json new file mode 100644 index 00000000000000..02556077e2df52 --- /dev/null +++ b/homeassistant/components/velux/strings.json @@ -0,0 +1,24 @@ +{ + "title": "Velux KLF200 Gateway", + "config": { + "step": { + "user": { + "title": "Configure your KLF200 Gateway", + "description": "Please enter your password, you will find it on the backside of the KLF200", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_host": "[%key:common::config_flow::error::invalid_host%]" + }, + "abort": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/velux/translations/en.json b/homeassistant/components/velux/translations/en.json new file mode 100644 index 00000000000000..397cefb7d05d1c --- /dev/null +++ b/homeassistant/components/velux/translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured_device": "Device is already configured", + "no_devices_found": "No devices found on the network" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_host": "Invalid hostname or IP address" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password" + }, + "description": "Please enter your password, you will find it on the backside of the KLF200", + "title": "Configure your KLF200 Gateway" + } + } + }, + "title": "Velux KLF200 Gateway" +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 05ce927c7739dd..110f43bb24979c 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -195,6 +195,7 @@ "upb", "upnp", "velbus", + "velux", "vera", "vesync", "vilfo", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index ba12b4ec4dec12..50b772d7b4b46e 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -38,7 +38,8 @@ "homekit_controller" ], "_http._tcp.local.": [ - "shelly" + "shelly", + "velux" ], "_ipp._tcp.local.": [ "ipp" diff --git a/requirements_all.txt b/requirements_all.txt index 6b6c2fcd070b67..497fb408d9394a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1833,7 +1833,7 @@ pyvesync==1.1.0 pyvizio==0.1.56 # homeassistant.components.velux -pyvlx==0.2.16 +pyvlx==0.2.18 # homeassistant.components.volumio pyvolumio==0.1.2