From 38b976e6d61e0cc94d9016e39a1fed749c0c6e4b Mon Sep 17 00:00:00 2001 From: Hans Oischinger <hans.oischinger@gmail.com> Date: Mon, 22 Nov 2021 15:06:42 +0100 Subject: [PATCH] Add vicare config flow (#56691) * Configuration via UI Config flow / YAML deprecation - Support discovery via MAC address - Support import of YAML config - Switch to ConfigEntry, get rid of platform setup * Fix review comments * More tests for vicare yaml import --- .coveragerc | 7 +- homeassistant/components/vicare/__init__.py | 83 ++++-- .../components/vicare/binary_sensor.py | 28 +- homeassistant/components/vicare/climate.py | 33 +-- .../components/vicare/config_flow.py | 110 +++++++ homeassistant/components/vicare/const.py | 1 - homeassistant/components/vicare/manifest.json | 8 +- homeassistant/components/vicare/sensor.py | 30 +- .../components/vicare/translations/en.json | 20 ++ .../components/vicare/water_heater.py | 29 +- homeassistant/generated/config_flows.py | 1 + homeassistant/generated/dhcp.py | 4 + requirements_test_all.txt | 3 + tests/components/vicare/__init__.py | 20 ++ tests/components/vicare/test_config_flow.py | 274 ++++++++++++++++++ 15 files changed, 560 insertions(+), 91 deletions(-) create mode 100644 homeassistant/components/vicare/config_flow.py create mode 100644 homeassistant/components/vicare/translations/en.json create mode 100644 tests/components/vicare/__init__.py create mode 100644 tests/components/vicare/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 0e01d0c7849..9591a8705d1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1194,7 +1194,12 @@ omit = homeassistant/components/vesync/light.py homeassistant/components/vesync/switch.py homeassistant/components/viaggiatreno/sensor.py - homeassistant/components/vicare/* + homeassistant/components/vicare/binary_sensor.py + homeassistant/components/vicare/climate.py + homeassistant/components/vicare/const.py + homeassistant/components/vicare/__init__.py + homeassistant/components/vicare/sensor.py + homeassistant/components/vicare/water_heater.py homeassistant/components/vilfo/__init__.py homeassistant/components/vilfo/sensor.py homeassistant/components/vilfo/const.py diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index c1571c2f91b..d4e4db3fbe9 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -9,6 +9,7 @@ from PyViCare.PyViCare import PyViCare from PyViCare.PyViCareDevice import Device import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_CLIENT_ID, CONF_NAME, @@ -16,7 +17,7 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_USERNAME, ) -from homeassistant.helpers import discovery +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.storage import STORAGE_DIR @@ -30,7 +31,6 @@ from .const import ( VICARE_API, VICARE_CIRCUITS, VICARE_DEVICE_CONFIG, - VICARE_NAME, HeatingType, ) @@ -61,8 +61,8 @@ CONFIG_SCHEMA = vol.Schema( ): int, # Ignored: All circuits are now supported. Will be removed when switching to Setup via UI. vol.Optional(CONF_NAME, default="ViCare"): cv.string, vol.Optional( - CONF_HEATING_TYPE, default=DEFAULT_HEATING_TYPE - ): cv.enum(HeatingType), + CONF_HEATING_TYPE, default=DEFAULT_HEATING_TYPE.value + ): vol.In([e.value for e in HeatingType]), } ), ) @@ -71,44 +71,75 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass, config): - """Create the ViCare component.""" - conf = config[DOMAIN] - params = {"token_file": hass.config.path(STORAGE_DIR, "vicare_token.save")} +async def async_setup(hass: HomeAssistant, config) -> bool: + """Set up the ViCare component from yaml.""" + if DOMAIN not in config: + # Setup via UI. No need to continue yaml-based setup + return True - params["cacheDuration"] = conf.get(CONF_SCAN_INTERVAL) - params["client_id"] = conf.get(CONF_CLIENT_ID) + 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, entry: ConfigEntry) -> bool: + """Set up from config entry.""" + _LOGGER.debug("Setting up ViCare component") hass.data[DOMAIN] = {} - hass.data[DOMAIN][VICARE_NAME] = conf[CONF_NAME] - setup_vicare_api(hass, conf, hass.data[DOMAIN]) + hass.data[DOMAIN][entry.entry_id] = {} - hass.data[DOMAIN][CONF_HEATING_TYPE] = conf[CONF_HEATING_TYPE] + await hass.async_add_executor_job(setup_vicare_api, hass, entry) - for platform in PLATFORMS: - discovery.load_platform(hass, platform, DOMAIN, {}, config) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -def setup_vicare_api(hass, conf, entity_data): - """Set up PyVicare API.""" +def vicare_login(hass, entry_data): + """Login via PyVicare API.""" vicare_api = PyViCare() - vicare_api.setCacheDuration(conf[CONF_SCAN_INTERVAL]) + vicare_api.setCacheDuration(entry_data[CONF_SCAN_INTERVAL]) vicare_api.initWithCredentials( - conf[CONF_USERNAME], - conf[CONF_PASSWORD], - conf[CONF_CLIENT_ID], + entry_data[CONF_USERNAME], + entry_data[CONF_PASSWORD], + entry_data[CONF_CLIENT_ID], hass.config.path(STORAGE_DIR, "vicare_token.save"), ) + return vicare_api + + +def setup_vicare_api(hass, entry): + """Set up PyVicare API.""" + vicare_api = vicare_login(hass, entry.data) - device = vicare_api.devices[0] for device in vicare_api.devices: _LOGGER.info( "Found device: %s (online: %s)", device.getModel(), str(device.isOnline()) ) - entity_data[VICARE_DEVICE_CONFIG] = device - entity_data[VICARE_API] = getattr( - device, HEATING_TYPE_TO_CREATOR_METHOD[conf[CONF_HEATING_TYPE]] + + # Currently we only support a single device + device = vicare_api.devices[0] + hass.data[DOMAIN][entry.entry_id][VICARE_DEVICE_CONFIG] = device + hass.data[DOMAIN][entry.entry_id][VICARE_API] = getattr( + device, + HEATING_TYPE_TO_CREATOR_METHOD[HeatingType(entry.data[CONF_HEATING_TYPE])], )() - entity_data[VICARE_CIRCUITS] = entity_data[VICARE_API].circuits + hass.data[DOMAIN][entry.entry_id][VICARE_CIRCUITS] = hass.data[DOMAIN][ + entry.entry_id + ][VICARE_API].circuits + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload ViCare config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/vicare/binary_sensor.py b/homeassistant/components/vicare/binary_sensor.py index 4484d5f5040..510c332f89f 100644 --- a/homeassistant/components/vicare/binary_sensor.py +++ b/homeassistant/components/vicare/binary_sensor.py @@ -17,9 +17,10 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) +from homeassistant.const import CONF_NAME from . import ViCareRequiredKeysMixin -from .const import DOMAIN, VICARE_API, VICARE_DEVICE_CONFIG, VICARE_NAME +from .const import DOMAIN, VICARE_API, VICARE_CIRCUITS, VICARE_DEVICE_CONFIG _LOGGER = logging.getLogger(__name__) @@ -84,7 +85,7 @@ def _build_entity(name, vicare_api, device_config, sensor): async def _entities_from_descriptions( - hass, name, all_devices, sensor_descriptions, iterables + hass, name, all_devices, sensor_descriptions, iterables, config_entry ): """Create entities from descriptions and list of burners/circuits.""" for description in sensor_descriptions: @@ -96,33 +97,30 @@ async def _entities_from_descriptions( _build_entity, f"{name} {description.name}{suffix}", current, - hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG], description, ) if entity is not None: all_devices.append(entity) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_devices): """Create the ViCare binary sensor devices.""" - if discovery_info is None: - return - - name = hass.data[DOMAIN][VICARE_NAME] - api = hass.data[DOMAIN][VICARE_API] + name = config_entry.data[CONF_NAME] + api = hass.data[DOMAIN][config_entry.entry_id][VICARE_API] all_devices = [] for description in CIRCUIT_SENSORS: - for circuit in api.circuits: + for circuit in hass.data[DOMAIN][config_entry.entry_id][VICARE_CIRCUITS]: suffix = "" - if len(api.circuits) > 1: + if len(hass.data[DOMAIN][config_entry.entry_id][VICARE_CIRCUITS]) > 1: suffix = f" {circuit.id}" entity = await hass.async_add_executor_job( _build_entity, f"{name} {description.name}{suffix}", circuit, - hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG], description, ) if entity is not None: @@ -130,19 +128,19 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= try: await _entities_from_descriptions( - hass, name, all_devices, BURNER_SENSORS, api.burners + hass, name, all_devices, BURNER_SENSORS, api.burners, config_entry ) except PyViCareNotSupportedFeatureError: _LOGGER.info("No burners found") try: await _entities_from_descriptions( - hass, name, all_devices, COMPRESSOR_SENSORS, api.compressors + hass, name, all_devices, COMPRESSOR_SENSORS, api.compressors, config_entry ) except PyViCareNotSupportedFeatureError: _LOGGER.info("No compressors found") - async_add_entities(all_devices) + async_add_devices(all_devices) class ViCareBinarySensor(BinarySensorEntity): diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index cdc5e826cab..1521ad1cf89 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -22,7 +22,12 @@ from homeassistant.components.climate.const import ( SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_NAME, + PRECISION_WHOLE, + TEMP_CELSIUS, +) from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv @@ -32,7 +37,6 @@ from .const import ( VICARE_API, VICARE_CIRCUITS, VICARE_DEVICE_CONFIG, - VICARE_NAME, ) _LOGGER = logging.getLogger(__name__) @@ -99,33 +103,26 @@ def _build_entity(name, vicare_api, circuit, device_config, heating_type): return ViCareClimate(name, vicare_api, device_config, circuit, heating_type) -async def async_setup_platform( - hass, hass_config, async_add_entities, discovery_info=None -): - """Create the ViCare climate devices.""" - # Legacy setup. Remove after configuration.yaml deprecation end - if discovery_info is None: - return +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the ViCare climate platform.""" + name = config_entry.data[CONF_NAME] - name = hass.data[DOMAIN][VICARE_NAME] all_devices = [] - for circuit in hass.data[DOMAIN][VICARE_CIRCUITS]: + for circuit in hass.data[DOMAIN][config_entry.entry_id][VICARE_CIRCUITS]: suffix = "" - if len(hass.data[DOMAIN][VICARE_CIRCUITS]) > 1: + if len(hass.data[DOMAIN][config_entry.entry_id][VICARE_CIRCUITS]) > 1: suffix = f" {circuit.id}" entity = _build_entity( f"{name} Heating{suffix}", - hass.data[DOMAIN][VICARE_API], - hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + hass.data[DOMAIN][config_entry.entry_id][VICARE_API], + hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG], circuit, - hass.data[DOMAIN][CONF_HEATING_TYPE], + config_entry.data[CONF_HEATING_TYPE], ) if entity is not None: all_devices.append(entity) - async_add_entities(all_devices) - platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( @@ -134,6 +131,8 @@ async def async_setup_platform( "set_vicare_mode", ) + async_add_devices(all_devices) + class ViCareClimate(ClimateEntity): """Representation of the ViCare heating climate device.""" diff --git a/homeassistant/components/vicare/config_flow.py b/homeassistant/components/vicare/config_flow.py new file mode 100644 index 00000000000..659b90b6dad --- /dev/null +++ b/homeassistant/components/vicare/config_flow.py @@ -0,0 +1,110 @@ +"""Config flow for ViCare integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from PyViCare.PyViCareUtils import PyViCareInvalidCredentialsError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.dhcp import MAC_ADDRESS +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_NAME, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import format_mac + +from . import vicare_login +from .const import ( + CONF_CIRCUIT, + CONF_HEATING_TYPE, + DEFAULT_HEATING_TYPE, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + HeatingType, +) + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for ViCare.""" + + VERSION = 1 + + async def async_step_user(self, user_input: dict[str, Any] | None = None): + """Invoke when a user initiates a flow via the user interface.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + data_schema = { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_HEATING_TYPE, default=DEFAULT_HEATING_TYPE.value): vol.In( + [e.value for e in HeatingType] + ), + vol.Optional(CONF_NAME, default="ViCare"): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): vol.All( + vol.Coerce(int), vol.Range(min=30) + ), + } + errors: dict[str, str] = {} + + if user_input is not None: + try: + await self.hass.async_add_executor_job( + vicare_login, self.hass, user_input + ) + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) + except PyViCareInvalidCredentialsError as ex: + _LOGGER.debug("Could not log in to ViCare, %s", ex) + errors["base"] = "invalid_auth" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema(data_schema), + errors=errors, + ) + + async def async_step_dhcp(self, discovery_info): + """Invoke when a Viessmann MAC address is discovered on the network.""" + formatted_mac = format_mac(discovery_info[MAC_ADDRESS]) + _LOGGER.info("Found device with mac %s", formatted_mac) + + await self.async_set_unique_id(formatted_mac) + self._abort_if_unique_id_configured() + + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + return await self.async_step_user() + + async def async_step_import(self, import_info): + """Handle a flow initiated by a YAML config import.""" + + await self.async_set_unique_id("Configuration.yaml") + self._abort_if_unique_id_configured() + + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + # Remove now unsupported config parameters + if import_info.get(CONF_CIRCUIT): + import_info.pop(CONF_CIRCUIT) + + # Add former optional config if missing + if import_info.get(CONF_HEATING_TYPE) is None: + import_info[CONF_HEATING_TYPE] = DEFAULT_HEATING_TYPE.value + + return self.async_create_entry( + title="Configuration.yaml", + data=import_info, + ) diff --git a/homeassistant/components/vicare/const.py b/homeassistant/components/vicare/const.py index 0cae3f7e0d1..db8b782399e 100644 --- a/homeassistant/components/vicare/const.py +++ b/homeassistant/components/vicare/const.py @@ -14,7 +14,6 @@ PLATFORMS = ["climate", "sensor", "binary_sensor", "water_heater"] VICARE_DEVICE_CONFIG = "device_conf" VICARE_API = "api" -VICARE_NAME = "name" VICARE_CIRCUITS = "circuits" CONF_CIRCUIT = "circuit" diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index 0fe1c1f95e2..dda2ed63a89 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -4,5 +4,11 @@ "documentation": "https://www.home-assistant.io/integrations/vicare", "codeowners": ["@oischinger"], "requirements": ["PyViCare==2.13.1"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "config_flow": true, + "dhcp": [ + { + "macaddress": "B87424*" + } + ] } diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 2ff8ce4bf7d..50ad64ab0c8 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -21,6 +21,7 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.const import ( + CONF_NAME, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, @@ -36,8 +37,8 @@ from . import ViCareRequiredKeysMixin from .const import ( DOMAIN, VICARE_API, + VICARE_CIRCUITS, VICARE_DEVICE_CONFIG, - VICARE_NAME, VICARE_UNIT_TO_DEVICE_CLASS, VICARE_UNIT_TO_UNIT_OF_MEASUREMENT, ) @@ -338,7 +339,7 @@ def _build_entity(name, vicare_api, device_config, sensor): async def _entities_from_descriptions( - hass, name, all_devices, sensor_descriptions, iterables + hass, name, all_devices, sensor_descriptions, iterables, config_entry ): """Create entities from descriptions and list of burners/circuits.""" for description in sensor_descriptions: @@ -350,20 +351,17 @@ async def _entities_from_descriptions( _build_entity, f"{name} {description.name}{suffix}", current, - hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG], description, ) if entity is not None: all_devices.append(entity) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_devices): """Create the ViCare sensor devices.""" - if discovery_info is None: - return - - name = hass.data[DOMAIN][VICARE_NAME] - api = hass.data[DOMAIN][VICARE_API] + name = config_entry.data[CONF_NAME] + api = hass.data[DOMAIN][config_entry.entry_id][VICARE_API] all_devices = [] for description in GLOBAL_SENSORS: @@ -371,22 +369,22 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= _build_entity, f"{name} {description.name}", api, - hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG], description, ) if entity is not None: all_devices.append(entity) for description in CIRCUIT_SENSORS: - for circuit in api.circuits: + for circuit in hass.data[DOMAIN][config_entry.entry_id][VICARE_CIRCUITS]: suffix = "" - if len(api.circuits) > 1: + if len(hass.data[DOMAIN][config_entry.entry_id][VICARE_CIRCUITS]) > 1: suffix = f" {circuit.id}" entity = await hass.async_add_executor_job( _build_entity, f"{name} {description.name}{suffix}", circuit, - hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG], description, ) if entity is not None: @@ -394,19 +392,19 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= try: await _entities_from_descriptions( - hass, name, all_devices, BURNER_SENSORS, api.burners + hass, name, all_devices, BURNER_SENSORS, api.burners, config_entry ) except PyViCareNotSupportedFeatureError: _LOGGER.info("No burners found") try: await _entities_from_descriptions( - hass, name, all_devices, COMPRESSOR_SENSORS, api.compressors + hass, name, all_devices, COMPRESSOR_SENSORS, api.compressors, config_entry ) except PyViCareNotSupportedFeatureError: _LOGGER.info("No compressors found") - async_add_entities(all_devices) + async_add_devices(all_devices) class ViCareSensor(SensorEntity): diff --git a/homeassistant/components/vicare/translations/en.json b/homeassistant/components/vicare/translations/en.json new file mode 100644 index 00000000000..b6ce73e1c2b --- /dev/null +++ b/homeassistant/components/vicare/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "data": { + "password": "Password", + "client_id": "API Key", + "username": "Username", + "heating_type": "Heating type" + }, + "description": "Setup ViCare to control your Viessmann device.\nMinimum needed: username, password, API key.", + "title": "Setup ViCare" + } + }, + "error": { + "invalid_auth": "Invalid authentication" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index 883bcd3efd5..1b98ad31992 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -13,7 +13,12 @@ from homeassistant.components.water_heater import ( SUPPORT_TARGET_TEMPERATURE, WaterHeaterEntity, ) -from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_NAME, + PRECISION_WHOLE, + TEMP_CELSIUS, +) from .const import ( CONF_HEATING_TYPE, @@ -21,7 +26,6 @@ from .const import ( VICARE_API, VICARE_CIRCUITS, VICARE_DEVICE_CONFIG, - VICARE_NAME, ) _LOGGER = logging.getLogger(__name__) @@ -66,29 +70,26 @@ def _build_entity(name, vicare_api, circuit, device_config, heating_type): ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Create the ViCare water_heater devices.""" - if discovery_info is None: - return - - name = hass.data[DOMAIN][VICARE_NAME] +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the ViCare climate platform.""" + name = config_entry.data[CONF_NAME] all_devices = [] - for circuit in hass.data[DOMAIN][VICARE_CIRCUITS]: + for circuit in hass.data[DOMAIN][config_entry.entry_id][VICARE_CIRCUITS]: suffix = "" - if len(hass.data[DOMAIN][VICARE_CIRCUITS]) > 1: + if len(hass.data[DOMAIN][config_entry.entry_id][VICARE_CIRCUITS]) > 1: suffix = f" {circuit.id}" entity = _build_entity( f"{name} Water{suffix}", - hass.data[DOMAIN][VICARE_API], + hass.data[DOMAIN][config_entry.entry_id][VICARE_API], circuit, - hass.data[DOMAIN][VICARE_DEVICE_CONFIG], - hass.data[DOMAIN][CONF_HEATING_TYPE], + hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG], + config_entry.data[CONF_HEATING_TYPE], ) if entity is not None: all_devices.append(entity) - async_add_entities(all_devices) + async_add_devices(all_devices) class ViCareWater(WaterHeaterEntity): diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 4d9cb7d7bc4..35bc6269d6c 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -318,6 +318,7 @@ FLOWS = [ "vera", "verisure", "vesync", + "vicare", "vilfo", "vizio", "vlc_telnet", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index f11cf63df9c..31481df3495 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -544,6 +544,10 @@ DHCP = [ "domain": "verisure", "macaddress": "0023C1*" }, + { + "domain": "vicare", + "macaddress": "B87424*" + }, { "domain": "yeelight", "hostname": "yeelink-*" diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7acbdb7674c..1b18ccac32d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -32,6 +32,9 @@ PyTransportNSW==0.1.1 # homeassistant.components.camera PyTurboJPEG==1.6.1 +# homeassistant.components.vicare +PyViCare==2.13.1 + # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4 diff --git a/tests/components/vicare/__init__.py b/tests/components/vicare/__init__.py new file mode 100644 index 00000000000..f67e50be1d6 --- /dev/null +++ b/tests/components/vicare/__init__.py @@ -0,0 +1,20 @@ +"""Test for ViCare.""" +from homeassistant.components.vicare.const import CONF_HEATING_TYPE +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_NAME, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) + +ENTRY_CONFIG = { + CONF_USERNAME: "foo@bar.com", + CONF_PASSWORD: "1234", + CONF_CLIENT_ID: "5678", + CONF_HEATING_TYPE: "auto", + CONF_SCAN_INTERVAL: 60, + CONF_NAME: "ViCare", +} + +MOCK_MAC = "B874241B7B9" diff --git a/tests/components/vicare/test_config_flow.py b/tests/components/vicare/test_config_flow.py new file mode 100644 index 00000000000..6d977934de3 --- /dev/null +++ b/tests/components/vicare/test_config_flow.py @@ -0,0 +1,274 @@ +"""Test the ViCare config flow.""" +from unittest.mock import patch + +from PyViCare.PyViCareUtils import PyViCareInvalidCredentialsError + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components import dhcp +from homeassistant.components.vicare.const import ( + CONF_CIRCUIT, + CONF_HEATING_TYPE, + DOMAIN, +) +from homeassistant.const import CONF_CLIENT_ID, CONF_PASSWORD, CONF_USERNAME + +from . import ENTRY_CONFIG, MOCK_MAC + +from tests.common import MockConfigEntry + + +async def test_form(hass): + """Test we get the form.""" + 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"] == data_entry_flow.RESULT_TYPE_FORM + assert len(result["errors"]) == 0 + + with patch( + "homeassistant.components.vicare.config_flow.vicare_login", + return_value=None, + ), patch( + "homeassistant.components.vicare.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.vicare.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "foo@bar.com", + CONF_PASSWORD: "1234", + CONF_CLIENT_ID: "5678", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "ViCare" + assert result2["data"] == ENTRY_CONFIG + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import(hass): + """Test that the import works.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.vicare.config_flow.vicare_login", + return_value=True, + ), patch( + "homeassistant.components.vicare.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.vicare.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=ENTRY_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Configuration.yaml" + assert result["data"] == ENTRY_CONFIG + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_removes_circuit(hass): + """Test that the import works.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.vicare.config_flow.vicare_login", + return_value=True, + ), patch( + "homeassistant.components.vicare.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.vicare.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + ENTRY_CONFIG[CONF_CIRCUIT] = 1 + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=ENTRY_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Configuration.yaml" + assert result["data"] == ENTRY_CONFIG + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_adds_heating_type(hass): + """Test that the import works.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.vicare.config_flow.vicare_login", + return_value=True, + ), patch( + "homeassistant.components.vicare.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.vicare.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + del ENTRY_CONFIG[CONF_HEATING_TYPE] + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=ENTRY_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Configuration.yaml" + assert result["data"] == ENTRY_CONFIG + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_invalid_login(hass) -> None: + """Test a flow with an invalid Vicare login.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.vicare.config_flow.vicare_login", + side_effect=PyViCareInvalidCredentialsError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "foo@bar.com", + CONF_PASSWORD: "1234", + CONF_CLIENT_ID: "5678", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_dhcp(hass): + """Test we can setup from dhcp.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + dhcp.MAC_ADDRESS: MOCK_MAC, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.vicare.config_flow.vicare_login", + return_value=None, + ), patch( + "homeassistant.components.vicare.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.vicare.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "foo@bar.com", + CONF_PASSWORD: "1234", + CONF_CLIENT_ID: "5678", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "ViCare" + assert result2["data"] == ENTRY_CONFIG + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_already_configured(hass): + """Test that configuring same instance is rejectes.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="Configuration.yaml", + data=ENTRY_CONFIG, + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=ENTRY_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_import_single_instance_allowed(hass): + """Test that configuring more than one instance is rejected.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="Configuration.yaml", + data=ENTRY_CONFIG, + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=ENTRY_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_dhcp_single_instance_allowed(hass): + """Test that configuring more than one instance is rejected.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="Configuration.yaml", + data=ENTRY_CONFIG, + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + dhcp.MAC_ADDRESS: MOCK_MAC, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" + + +async def test_user_input_single_instance_allowed(hass): + """Test that configuring more than one instance is rejected.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="ViCare", + data=ENTRY_CONFIG, + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" -- GitLab