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