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
This commit is contained in:
Hans Oischinger 2021-11-22 15:06:42 +01:00 committed by GitHub
parent a7382c8092
commit 38b976e6d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 564 additions and 95 deletions

View file

@ -1194,7 +1194,12 @@ omit =
homeassistant/components/vesync/light.py homeassistant/components/vesync/light.py
homeassistant/components/vesync/switch.py homeassistant/components/vesync/switch.py
homeassistant/components/viaggiatreno/sensor.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/__init__.py
homeassistant/components/vilfo/sensor.py homeassistant/components/vilfo/sensor.py
homeassistant/components/vilfo/const.py homeassistant/components/vilfo/const.py

View file

@ -9,6 +9,7 @@ from PyViCare.PyViCare import PyViCare
from PyViCare.PyViCareDevice import Device from PyViCare.PyViCareDevice import Device
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_CLIENT_ID, CONF_CLIENT_ID,
CONF_NAME, CONF_NAME,
@ -16,7 +17,7 @@ from homeassistant.const import (
CONF_SCAN_INTERVAL, CONF_SCAN_INTERVAL,
CONF_USERNAME, CONF_USERNAME,
) )
from homeassistant.helpers import discovery from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.storage import STORAGE_DIR
@ -30,7 +31,6 @@ from .const import (
VICARE_API, VICARE_API,
VICARE_CIRCUITS, VICARE_CIRCUITS,
VICARE_DEVICE_CONFIG, VICARE_DEVICE_CONFIG,
VICARE_NAME,
HeatingType, 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. ): 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_NAME, default="ViCare"): cv.string,
vol.Optional( vol.Optional(
CONF_HEATING_TYPE, default=DEFAULT_HEATING_TYPE CONF_HEATING_TYPE, default=DEFAULT_HEATING_TYPE.value
): cv.enum(HeatingType), ): vol.In([e.value for e in HeatingType]),
} }
), ),
) )
@ -71,44 +71,75 @@ CONFIG_SCHEMA = vol.Schema(
) )
def setup(hass, config): async def async_setup(hass: HomeAssistant, config) -> bool:
"""Create the ViCare component.""" """Set up the ViCare component from yaml."""
conf = config[DOMAIN] if DOMAIN not in config:
params = {"token_file": hass.config.path(STORAGE_DIR, "vicare_token.save")} # Setup via UI. No need to continue yaml-based setup
return True
params["cacheDuration"] = conf.get(CONF_SCAN_INTERVAL) hass.async_create_task(
params["client_id"] = conf.get(CONF_CLIENT_ID) hass.config_entries.flow.async_init(
DOMAIN,
hass.data[DOMAIN] = {} context={"source": SOURCE_IMPORT},
hass.data[DOMAIN][VICARE_NAME] = conf[CONF_NAME] data=config[DOMAIN],
setup_vicare_api(hass, conf, hass.data[DOMAIN]) )
)
hass.data[DOMAIN][CONF_HEATING_TYPE] = conf[CONF_HEATING_TYPE]
for platform in PLATFORMS:
discovery.load_platform(hass, platform, DOMAIN, {}, config)
return True return True
def setup_vicare_api(hass, conf, entity_data): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up PyVicare API.""" """Set up from config entry."""
_LOGGER.debug("Setting up ViCare component")
hass.data[DOMAIN] = {}
hass.data[DOMAIN][entry.entry_id] = {}
await hass.async_add_executor_job(setup_vicare_api, hass, entry)
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
def vicare_login(hass, entry_data):
"""Login via PyVicare API."""
vicare_api = PyViCare() vicare_api = PyViCare()
vicare_api.setCacheDuration(conf[CONF_SCAN_INTERVAL]) vicare_api.setCacheDuration(entry_data[CONF_SCAN_INTERVAL])
vicare_api.initWithCredentials( vicare_api.initWithCredentials(
conf[CONF_USERNAME], entry_data[CONF_USERNAME],
conf[CONF_PASSWORD], entry_data[CONF_PASSWORD],
conf[CONF_CLIENT_ID], entry_data[CONF_CLIENT_ID],
hass.config.path(STORAGE_DIR, "vicare_token.save"), 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: for device in vicare_api.devices:
_LOGGER.info( _LOGGER.info(
"Found device: %s (online: %s)", device.getModel(), str(device.isOnline()) "Found device: %s (online: %s)", device.getModel(), str(device.isOnline())
) )
entity_data[VICARE_DEVICE_CONFIG] = device
entity_data[VICARE_API] = getattr( # Currently we only support a single device
device, HEATING_TYPE_TO_CREATOR_METHOD[conf[CONF_HEATING_TYPE]] 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

View file

@ -17,9 +17,10 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity, BinarySensorEntity,
BinarySensorEntityDescription, BinarySensorEntityDescription,
) )
from homeassistant.const import CONF_NAME
from . import ViCareRequiredKeysMixin 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__) _LOGGER = logging.getLogger(__name__)
@ -84,7 +85,7 @@ def _build_entity(name, vicare_api, device_config, sensor):
async def _entities_from_descriptions( 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.""" """Create entities from descriptions and list of burners/circuits."""
for description in sensor_descriptions: for description in sensor_descriptions:
@ -96,33 +97,30 @@ async def _entities_from_descriptions(
_build_entity, _build_entity,
f"{name} {description.name}{suffix}", f"{name} {description.name}{suffix}",
current, current,
hass.data[DOMAIN][VICARE_DEVICE_CONFIG], hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG],
description, description,
) )
if entity is not None: if entity is not None:
all_devices.append(entity) 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.""" """Create the ViCare binary sensor devices."""
if discovery_info is None: name = config_entry.data[CONF_NAME]
return api = hass.data[DOMAIN][config_entry.entry_id][VICARE_API]
name = hass.data[DOMAIN][VICARE_NAME]
api = hass.data[DOMAIN][VICARE_API]
all_devices = [] all_devices = []
for description in CIRCUIT_SENSORS: for description in CIRCUIT_SENSORS:
for circuit in api.circuits: for circuit in hass.data[DOMAIN][config_entry.entry_id][VICARE_CIRCUITS]:
suffix = "" suffix = ""
if len(api.circuits) > 1: if len(hass.data[DOMAIN][config_entry.entry_id][VICARE_CIRCUITS]) > 1:
suffix = f" {circuit.id}" suffix = f" {circuit.id}"
entity = await hass.async_add_executor_job( entity = await hass.async_add_executor_job(
_build_entity, _build_entity,
f"{name} {description.name}{suffix}", f"{name} {description.name}{suffix}",
circuit, circuit,
hass.data[DOMAIN][VICARE_DEVICE_CONFIG], hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG],
description, description,
) )
if entity is not None: if entity is not None:
@ -130,19 +128,19 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
try: try:
await _entities_from_descriptions( await _entities_from_descriptions(
hass, name, all_devices, BURNER_SENSORS, api.burners hass, name, all_devices, BURNER_SENSORS, api.burners, config_entry
) )
except PyViCareNotSupportedFeatureError: except PyViCareNotSupportedFeatureError:
_LOGGER.info("No burners found") _LOGGER.info("No burners found")
try: try:
await _entities_from_descriptions( await _entities_from_descriptions(
hass, name, all_devices, COMPRESSOR_SENSORS, api.compressors hass, name, all_devices, COMPRESSOR_SENSORS, api.compressors, config_entry
) )
except PyViCareNotSupportedFeatureError: except PyViCareNotSupportedFeatureError:
_LOGGER.info("No compressors found") _LOGGER.info("No compressors found")
async_add_entities(all_devices) async_add_devices(all_devices)
class ViCareBinarySensor(BinarySensorEntity): class ViCareBinarySensor(BinarySensorEntity):

View file

@ -22,7 +22,12 @@ from homeassistant.components.climate.const import (
SUPPORT_PRESET_MODE, SUPPORT_PRESET_MODE,
SUPPORT_TARGET_TEMPERATURE, 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 from homeassistant.helpers import entity_platform
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -32,7 +37,6 @@ from .const import (
VICARE_API, VICARE_API,
VICARE_CIRCUITS, VICARE_CIRCUITS,
VICARE_DEVICE_CONFIG, VICARE_DEVICE_CONFIG,
VICARE_NAME,
) )
_LOGGER = logging.getLogger(__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) return ViCareClimate(name, vicare_api, device_config, circuit, heating_type)
async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_devices):
hass, hass_config, async_add_entities, discovery_info=None """Set up the ViCare climate platform."""
): name = config_entry.data[CONF_NAME]
"""Create the ViCare climate devices."""
# Legacy setup. Remove after configuration.yaml deprecation end
if discovery_info is None:
return
name = hass.data[DOMAIN][VICARE_NAME]
all_devices = [] all_devices = []
for circuit in hass.data[DOMAIN][VICARE_CIRCUITS]: for circuit in hass.data[DOMAIN][config_entry.entry_id][VICARE_CIRCUITS]:
suffix = "" 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}" suffix = f" {circuit.id}"
entity = _build_entity( entity = _build_entity(
f"{name} Heating{suffix}", f"{name} Heating{suffix}",
hass.data[DOMAIN][VICARE_API], hass.data[DOMAIN][config_entry.entry_id][VICARE_API],
hass.data[DOMAIN][VICARE_DEVICE_CONFIG], hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG],
circuit, circuit,
hass.data[DOMAIN][CONF_HEATING_TYPE], config_entry.data[CONF_HEATING_TYPE],
) )
if entity is not None: if entity is not None:
all_devices.append(entity) all_devices.append(entity)
async_add_entities(all_devices)
platform = entity_platform.async_get_current_platform() platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service( platform.async_register_entity_service(
@ -134,6 +131,8 @@ async def async_setup_platform(
"set_vicare_mode", "set_vicare_mode",
) )
async_add_devices(all_devices)
class ViCareClimate(ClimateEntity): class ViCareClimate(ClimateEntity):
"""Representation of the ViCare heating climate device.""" """Representation of the ViCare heating climate device."""

View file

@ -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,
)

View file

@ -14,7 +14,6 @@ PLATFORMS = ["climate", "sensor", "binary_sensor", "water_heater"]
VICARE_DEVICE_CONFIG = "device_conf" VICARE_DEVICE_CONFIG = "device_conf"
VICARE_API = "api" VICARE_API = "api"
VICARE_NAME = "name"
VICARE_CIRCUITS = "circuits" VICARE_CIRCUITS = "circuits"
CONF_CIRCUIT = "circuit" CONF_CIRCUIT = "circuit"

View file

@ -4,5 +4,11 @@
"documentation": "https://www.home-assistant.io/integrations/vicare", "documentation": "https://www.home-assistant.io/integrations/vicare",
"codeowners": ["@oischinger"], "codeowners": ["@oischinger"],
"requirements": ["PyViCare==2.13.1"], "requirements": ["PyViCare==2.13.1"],
"iot_class": "cloud_polling" "iot_class": "cloud_polling",
"config_flow": true,
"dhcp": [
{
"macaddress": "B87424*"
}
]
} }

View file

@ -21,6 +21,7 @@ from homeassistant.components.sensor import (
SensorEntityDescription, SensorEntityDescription,
) )
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME,
DEVICE_CLASS_ENERGY, DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER, DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TEMPERATURE,
@ -36,8 +37,8 @@ from . import ViCareRequiredKeysMixin
from .const import ( from .const import (
DOMAIN, DOMAIN,
VICARE_API, VICARE_API,
VICARE_CIRCUITS,
VICARE_DEVICE_CONFIG, VICARE_DEVICE_CONFIG,
VICARE_NAME,
VICARE_UNIT_TO_DEVICE_CLASS, VICARE_UNIT_TO_DEVICE_CLASS,
VICARE_UNIT_TO_UNIT_OF_MEASUREMENT, VICARE_UNIT_TO_UNIT_OF_MEASUREMENT,
) )
@ -338,7 +339,7 @@ def _build_entity(name, vicare_api, device_config, sensor):
async def _entities_from_descriptions( 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.""" """Create entities from descriptions and list of burners/circuits."""
for description in sensor_descriptions: for description in sensor_descriptions:
@ -350,20 +351,17 @@ async def _entities_from_descriptions(
_build_entity, _build_entity,
f"{name} {description.name}{suffix}", f"{name} {description.name}{suffix}",
current, current,
hass.data[DOMAIN][VICARE_DEVICE_CONFIG], hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG],
description, description,
) )
if entity is not None: if entity is not None:
all_devices.append(entity) 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.""" """Create the ViCare sensor devices."""
if discovery_info is None: name = config_entry.data[CONF_NAME]
return api = hass.data[DOMAIN][config_entry.entry_id][VICARE_API]
name = hass.data[DOMAIN][VICARE_NAME]
api = hass.data[DOMAIN][VICARE_API]
all_devices = [] all_devices = []
for description in GLOBAL_SENSORS: for description in GLOBAL_SENSORS:
@ -371,22 +369,22 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
_build_entity, _build_entity,
f"{name} {description.name}", f"{name} {description.name}",
api, api,
hass.data[DOMAIN][VICARE_DEVICE_CONFIG], hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG],
description, description,
) )
if entity is not None: if entity is not None:
all_devices.append(entity) all_devices.append(entity)
for description in CIRCUIT_SENSORS: for description in CIRCUIT_SENSORS:
for circuit in api.circuits: for circuit in hass.data[DOMAIN][config_entry.entry_id][VICARE_CIRCUITS]:
suffix = "" suffix = ""
if len(api.circuits) > 1: if len(hass.data[DOMAIN][config_entry.entry_id][VICARE_CIRCUITS]) > 1:
suffix = f" {circuit.id}" suffix = f" {circuit.id}"
entity = await hass.async_add_executor_job( entity = await hass.async_add_executor_job(
_build_entity, _build_entity,
f"{name} {description.name}{suffix}", f"{name} {description.name}{suffix}",
circuit, circuit,
hass.data[DOMAIN][VICARE_DEVICE_CONFIG], hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG],
description, description,
) )
if entity is not None: if entity is not None:
@ -394,19 +392,19 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
try: try:
await _entities_from_descriptions( await _entities_from_descriptions(
hass, name, all_devices, BURNER_SENSORS, api.burners hass, name, all_devices, BURNER_SENSORS, api.burners, config_entry
) )
except PyViCareNotSupportedFeatureError: except PyViCareNotSupportedFeatureError:
_LOGGER.info("No burners found") _LOGGER.info("No burners found")
try: try:
await _entities_from_descriptions( await _entities_from_descriptions(
hass, name, all_devices, COMPRESSOR_SENSORS, api.compressors hass, name, all_devices, COMPRESSOR_SENSORS, api.compressors, config_entry
) )
except PyViCareNotSupportedFeatureError: except PyViCareNotSupportedFeatureError:
_LOGGER.info("No compressors found") _LOGGER.info("No compressors found")
async_add_entities(all_devices) async_add_devices(all_devices)
class ViCareSensor(SensorEntity): class ViCareSensor(SensorEntity):

View file

@ -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"
}
}
}

View file

@ -13,7 +13,12 @@ from homeassistant.components.water_heater import (
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE,
WaterHeaterEntity, 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 ( from .const import (
CONF_HEATING_TYPE, CONF_HEATING_TYPE,
@ -21,7 +26,6 @@ from .const import (
VICARE_API, VICARE_API,
VICARE_CIRCUITS, VICARE_CIRCUITS,
VICARE_DEVICE_CONFIG, VICARE_DEVICE_CONFIG,
VICARE_NAME,
) )
_LOGGER = logging.getLogger(__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): async def async_setup_entry(hass, config_entry, async_add_devices):
"""Create the ViCare water_heater devices.""" """Set up the ViCare climate platform."""
if discovery_info is None: name = config_entry.data[CONF_NAME]
return
name = hass.data[DOMAIN][VICARE_NAME]
all_devices = [] all_devices = []
for circuit in hass.data[DOMAIN][VICARE_CIRCUITS]: for circuit in hass.data[DOMAIN][config_entry.entry_id][VICARE_CIRCUITS]:
suffix = "" 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}" suffix = f" {circuit.id}"
entity = _build_entity( entity = _build_entity(
f"{name} Water{suffix}", f"{name} Water{suffix}",
hass.data[DOMAIN][VICARE_API], hass.data[DOMAIN][config_entry.entry_id][VICARE_API],
circuit, circuit,
hass.data[DOMAIN][VICARE_DEVICE_CONFIG], hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG],
hass.data[DOMAIN][CONF_HEATING_TYPE], config_entry.data[CONF_HEATING_TYPE],
) )
if entity is not None: if entity is not None:
all_devices.append(entity) all_devices.append(entity)
async_add_entities(all_devices) async_add_devices(all_devices)
class ViCareWater(WaterHeaterEntity): class ViCareWater(WaterHeaterEntity):

View file

@ -318,6 +318,7 @@ FLOWS = [
"vera", "vera",
"verisure", "verisure",
"vesync", "vesync",
"vicare",
"vilfo", "vilfo",
"vizio", "vizio",
"vlc_telnet", "vlc_telnet",

View file

@ -544,6 +544,10 @@ DHCP = [
"domain": "verisure", "domain": "verisure",
"macaddress": "0023C1*" "macaddress": "0023C1*"
}, },
{
"domain": "vicare",
"macaddress": "B87424*"
},
{ {
"domain": "yeelight", "domain": "yeelight",
"hostname": "yeelink-*" "hostname": "yeelink-*"

View file

@ -32,6 +32,9 @@ PyTransportNSW==0.1.1
# homeassistant.components.camera # homeassistant.components.camera
PyTurboJPEG==1.6.1 PyTurboJPEG==1.6.1
# homeassistant.components.vicare
PyViCare==2.13.1
# homeassistant.components.xiaomi_aqara # homeassistant.components.xiaomi_aqara
PyXiaomiGateway==0.13.4 PyXiaomiGateway==0.13.4

View file

@ -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"

View file

@ -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"