diff --git a/.coveragerc b/.coveragerc index 0e01d0c784997901739a540764d3c051bb7a5cc4..9591a8705d1e22d8b864716c4274aa9c6ee6dff4 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 c1571c2f91bc11b41846d40794b4625de9a58655..d4e4db3fbe97d0aad10aa126198c7b5b29dbdbf5 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 4484d5f504069f45f5f4735047aa155b064dea47..510c332f89f135891fd47c3d7320ed1febcf157c 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 cdc5e826cabccac8bbcf6cb2721fc8a0e964d001..1521ad1cf89edb9abd327b8bc5d70cc1fb5cf5ba 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 0000000000000000000000000000000000000000..659b90b6dadf90b30e0da54badf7ee6337d35e97 --- /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 0cae3f7e0d13ccea51c12592d9fd5145f65deb29..db8b782399e10fa4ae88b99d80b0581ed59d256d 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 0fe1c1f95e26681ca49171725ae54edcc4fc6074..dda2ed63a8913de425fcaadc612c85ce9e3bdac8 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 2ff8ce4bf7d5a6047cd631f6fb5167078c6fd5ea..50ad64ab0c8a56dacde4bb65ea2e1395b96c923b 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 0000000000000000000000000000000000000000..b6ce73e1c2b0e6e2143bd7684d940ab4e0f271d2 --- /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 883bcd3efd5d0eef0b1ae13f548f21b421096326..1b98ad319925b68b3787767902bb73635077e460 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 4d9cb7d7bc4a8bf9eb9ba89d9ff6d7ee780bd844..35bc6269d6cd33a12b96467b9ed6465f82d249a1 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 f11cf63df9cee38eb1e9d93a2167713b7842fd62..31481df34957beb5b1787bf1e5cc33b59e5837c3 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 7acbdb7674c808ffe80a1df964e48c12925a41ed..1b18ccac32d737641db8f795c5e0051a93196562 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 0000000000000000000000000000000000000000..f67e50be1d66703ea861264057ed019a7100182b --- /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 0000000000000000000000000000000000000000..6d977934de3300bb2e23753c305d04a08211afbc --- /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"