diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index b45716b33d6..0d34a521276 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -2,6 +2,7 @@ import asyncio from collections import defaultdict import logging +from typing import Type import pyvera as veraApi from requests.exceptions import RequestException @@ -19,17 +20,25 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import convert, slugify from homeassistant.util.dt import utc_from_timestamp -from .common import ControllerData, SubscriptionRegistry, get_configured_platforms +from .common import ( + ControllerData, + SubscriptionRegistry, + get_configured_platforms, + get_controller_data, + set_controller_data, +) from .config_flow import fix_device_id_list, new_options from .const import ( ATTR_CURRENT_ENERGY_KWH, ATTR_CURRENT_POWER_W, CONF_CONTROLLER, + CONF_LEGACY_UNIQUE_ID, DOMAIN, VERA_ID_FORMAT, ) @@ -54,6 +63,8 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, base_config: dict) -> bool: """Set up for Vera controllers.""" + hass.data[DOMAIN] = {} + config = base_config.get(DOMAIN) if not config: @@ -107,10 +118,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b all_devices = await hass.async_add_executor_job(controller.get_devices) all_scenes = await hass.async_add_executor_job(controller.get_scenes) - except RequestException: + except RequestException as exception: # There was a network related error connecting to the Vera controller. _LOGGER.exception("Error communicating with Vera API") - return False + raise ConfigEntryNotReady from exception # Exclude devices unwanted by user. devices = [device for device in all_devices if device.device_id not in exclude_ids] @@ -118,20 +129,21 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b vera_devices = defaultdict(list) for device in devices: device_type = map_vera_device(device, light_ids) - if device_type is None: - continue - - vera_devices[device_type].append(device) + if device_type is not None: + vera_devices[device_type].append(device) vera_scenes = [] for scene in all_scenes: vera_scenes.append(scene) controller_data = ControllerData( - controller=controller, devices=vera_devices, scenes=vera_scenes + controller=controller, + devices=vera_devices, + scenes=vera_scenes, + config_entry=config_entry, ) - hass.data[DOMAIN] = controller_data + set_controller_data(hass, config_entry, controller_data) # Forward the config data to the necessary platforms. for platform in get_configured_platforms(controller_data): @@ -144,7 +156,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload Withings config entry.""" - controller_data: ControllerData = hass.data[DOMAIN] + controller_data: ControllerData = get_controller_data(hass, config_entry) tasks = [ hass.config_entries.async_forward_entry_unload(config_entry, platform) @@ -159,43 +171,52 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> def map_vera_device(vera_device, remap): """Map vera classes to Home Assistant types.""" - if isinstance(vera_device, veraApi.VeraDimmer): - return "light" - if isinstance(vera_device, veraApi.VeraBinarySensor): - return "binary_sensor" - if isinstance(vera_device, veraApi.VeraSensor): - return "sensor" - if isinstance(vera_device, veraApi.VeraArmableDevice): - return "switch" - if isinstance(vera_device, veraApi.VeraLock): - return "lock" - if isinstance(vera_device, veraApi.VeraThermostat): - return "climate" - if isinstance(vera_device, veraApi.VeraCurtain): - return "cover" - if isinstance(vera_device, veraApi.VeraSceneController): - return "sensor" - if isinstance(vera_device, veraApi.VeraSwitch): - if vera_device.device_id in remap: + type_map = { + veraApi.VeraDimmer: "light", + veraApi.VeraBinarySensor: "binary_sensor", + veraApi.VeraSensor: "sensor", + veraApi.VeraArmableDevice: "switch", + veraApi.VeraLock: "lock", + veraApi.VeraThermostat: "climate", + veraApi.VeraCurtain: "cover", + veraApi.VeraSceneController: "sensor", + veraApi.VeraSwitch: "switch", + } + + def map_special_case(instance_class: Type, entity_type: str) -> str: + if instance_class is veraApi.VeraSwitch and vera_device.device_id in remap: return "light" - return "switch" - return None + return entity_type + + return next( + iter( + map_special_case(instance_class, entity_type) + for instance_class, entity_type in type_map.items() + if isinstance(vera_device, instance_class) + ), + None, + ) class VeraDevice(Entity): """Representation of a Vera device entity.""" - def __init__(self, vera_device, controller): + def __init__(self, vera_device, controller_data: ControllerData): """Initialize the device.""" self.vera_device = vera_device - self.controller = controller + self.controller = controller_data.controller self._name = self.vera_device.name # Append device id to prevent name clashes in HA. self.vera_id = VERA_ID_FORMAT.format( - slugify(vera_device.name), vera_device.device_id + slugify(vera_device.name), vera_device.vera_device_id ) + if controller_data.config_entry.data.get(CONF_LEGACY_UNIQUE_ID): + self._unique_id = str(self.vera_device.vera_device_id) + else: + self._unique_id = f"vera_{controller_data.config_entry.unique_id}_{self.vera_device.vera_device_id}" + async def async_added_to_hass(self): """Subscribe to updates.""" self.controller.register(self.vera_device, self._update_callback) @@ -254,4 +275,4 @@ class VeraDevice(Entity): The Vera assigns a unique and immutable ID number to each device. """ - return str(self.vera_device.vera_device_id) + return self._unique_id diff --git a/homeassistant/components/vera/binary_sensor.py b/homeassistant/components/vera/binary_sensor.py index 557874f846a..7ab24e9544f 100644 --- a/homeassistant/components/vera/binary_sensor.py +++ b/homeassistant/components/vera/binary_sensor.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from . import VeraDevice -from .const import DOMAIN +from .common import ControllerData, get_controller_data _LOGGER = logging.getLogger(__name__) @@ -23,10 +23,10 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" - controller_data = hass.data[DOMAIN] + controller_data = get_controller_data(hass, entry) async_add_entities( [ - VeraBinarySensor(device, controller_data.controller) + VeraBinarySensor(device, controller_data) for device in controller_data.devices.get(PLATFORM_DOMAIN) ] ) @@ -35,10 +35,10 @@ async def async_setup_entry( class VeraBinarySensor(VeraDevice, BinarySensorEntity): """Representation of a Vera Binary Sensor.""" - def __init__(self, vera_device, controller): + def __init__(self, vera_device, controller_data: ControllerData): """Initialize the binary_sensor.""" self._state = False - VeraDevice.__init__(self, vera_device, controller) + VeraDevice.__init__(self, vera_device, controller_data) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) @property diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index 9b8601e45d1..a8ba647c1d6 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -24,7 +24,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import convert from . import VeraDevice -from .const import DOMAIN +from .common import ControllerData, get_controller_data _LOGGER = logging.getLogger(__name__) @@ -40,10 +40,10 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" - controller_data = hass.data[DOMAIN] + controller_data = get_controller_data(hass, entry) async_add_entities( [ - VeraThermostat(device, controller_data.controller) + VeraThermostat(device, controller_data) for device in controller_data.devices.get(PLATFORM_DOMAIN) ] ) @@ -52,9 +52,9 @@ async def async_setup_entry( class VeraThermostat(VeraDevice, ClimateEntity): """Representation of a Vera Thermostat.""" - def __init__(self, vera_device, controller): + def __init__(self, vera_device, controller_data: ControllerData): """Initialize the Vera device.""" - VeraDevice.__init__(self, vera_device, controller) + VeraDevice.__init__(self, vera_device, controller_data) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) @property diff --git a/homeassistant/components/vera/common.py b/homeassistant/components/vera/common.py index 17536bcae69..66a2d6879dd 100644 --- a/homeassistant/components/vera/common.py +++ b/homeassistant/components/vera/common.py @@ -5,9 +5,12 @@ from typing import DefaultDict, List, NamedTuple, Set import pyvera as pv from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.event import call_later +from .const import DOMAIN + _LOGGER = logging.getLogger(__name__) @@ -17,6 +20,7 @@ class ControllerData(NamedTuple): controller: pv.VeraController devices: DefaultDict[str, List[pv.VeraDevice]] scenes: List[pv.VeraScene] + config_entry: ConfigEntry def get_configured_platforms(controller_data: ControllerData) -> Set[str]: @@ -31,6 +35,20 @@ def get_configured_platforms(controller_data: ControllerData) -> Set[str]: return set(platforms) +def get_controller_data( + hass: HomeAssistant, config_entry: ConfigEntry +) -> ControllerData: + """Get controller data from hass data.""" + return hass.data[DOMAIN][config_entry.entry_id] + + +def set_controller_data( + hass: HomeAssistant, config_entry: ConfigEntry, data: ControllerData +) -> None: + """Set controller data in hass data.""" + hass.data[DOMAIN][config_entry.entry_id] = data + + class SubscriptionRegistry(pv.AbstractSubscriptionRegistry): """Manages polling for data from vera.""" diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py index a040e4b96b5..26ae509337b 100644 --- a/homeassistant/components/vera/config_flow.py +++ b/homeassistant/components/vera/config_flow.py @@ -10,8 +10,13 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE from homeassistant.core import callback +from homeassistant.helpers.entity_registry import EntityRegistry -from .const import CONF_CONTROLLER, DOMAIN +from .const import ( # pylint: disable=unused-import + CONF_CONTROLLER, + CONF_LEGACY_UNIQUE_ID, + DOMAIN, +) LIST_REGEX = re.compile("[^0-9]+") _LOGGER = logging.getLogger(__name__) @@ -92,15 +97,13 @@ class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input: dict = None): """Handle user initiated flow.""" - if self.hass.config_entries.async_entries(DOMAIN): - return self.async_abort(reason="already_configured") - if user_input is not None: return await self.async_step_finish( { **user_input, **options_data(user_input), **{CONF_SOURCE: config_entries.SOURCE_USER}, + **{CONF_LEGACY_UNIQUE_ID: False}, } ) @@ -113,8 +116,29 @@ class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, config: dict): """Handle a flow initialized by import.""" + + # If there are entities with the legacy unique_id, then this imported config + # should also use the legacy unique_id for entity creation. + entity_registry: EntityRegistry = ( + await self.hass.helpers.entity_registry.async_get_registry() + ) + use_legacy_unique_id = ( + len( + [ + entry + for entry in entity_registry.entities.values() + if entry.platform == DOMAIN and entry.unique_id.isdigit() + ] + ) + > 0 + ) + return await self.async_step_finish( - {**config, **{CONF_SOURCE: config_entries.SOURCE_IMPORT}} + { + **config, + **{CONF_SOURCE: config_entries.SOURCE_IMPORT}, + **{CONF_LEGACY_UNIQUE_ID: use_legacy_unique_id}, + } ) async def async_step_finish(self, config: dict): diff --git a/homeassistant/components/vera/const.py b/homeassistant/components/vera/const.py index c4f1d0efa3a..34ac7faa669 100644 --- a/homeassistant/components/vera/const.py +++ b/homeassistant/components/vera/const.py @@ -2,6 +2,7 @@ DOMAIN = "vera" CONF_CONTROLLER = "vera_controller_url" +CONF_LEGACY_UNIQUE_ID = "legacy_unique_id" VERA_ID_FORMAT = "{}_{}" diff --git a/homeassistant/components/vera/cover.py b/homeassistant/components/vera/cover.py index a1f536d9cc1..bad36727c15 100644 --- a/homeassistant/components/vera/cover.py +++ b/homeassistant/components/vera/cover.py @@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from . import VeraDevice -from .const import DOMAIN +from .common import ControllerData, get_controller_data _LOGGER = logging.getLogger(__name__) @@ -24,10 +24,10 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" - controller_data = hass.data[DOMAIN] + controller_data = get_controller_data(hass, entry) async_add_entities( [ - VeraCover(device, controller_data.controller) + VeraCover(device, controller_data) for device in controller_data.devices.get(PLATFORM_DOMAIN) ] ) @@ -36,9 +36,9 @@ async def async_setup_entry( class VeraCover(VeraDevice, CoverEntity): """Representation a Vera Cover.""" - def __init__(self, vera_device, controller): + def __init__(self, vera_device, controller_data: ControllerData): """Initialize the Vera device.""" - VeraDevice.__init__(self, vera_device, controller) + VeraDevice.__init__(self, vera_device, controller_data) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) @property diff --git a/homeassistant/components/vera/light.py b/homeassistant/components/vera/light.py index 250842f1687..84f36fe3877 100644 --- a/homeassistant/components/vera/light.py +++ b/homeassistant/components/vera/light.py @@ -17,7 +17,7 @@ from homeassistant.helpers.entity import Entity import homeassistant.util.color as color_util from . import VeraDevice -from .const import DOMAIN +from .common import ControllerData, get_controller_data _LOGGER = logging.getLogger(__name__) @@ -28,10 +28,10 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" - controller_data = hass.data[DOMAIN] + controller_data = get_controller_data(hass, entry) async_add_entities( [ - VeraLight(device, controller_data.controller) + VeraLight(device, controller_data) for device in controller_data.devices.get(PLATFORM_DOMAIN) ] ) @@ -40,12 +40,12 @@ async def async_setup_entry( class VeraLight(VeraDevice, LightEntity): """Representation of a Vera Light, including dimmable.""" - def __init__(self, vera_device, controller): + def __init__(self, vera_device, controller_data: ControllerData): """Initialize the light.""" self._state = False self._color = None self._brightness = None - VeraDevice.__init__(self, vera_device, controller) + VeraDevice.__init__(self, vera_device, controller_data) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) @property diff --git a/homeassistant/components/vera/lock.py b/homeassistant/components/vera/lock.py index f85beb5ba69..6a1158d18c4 100644 --- a/homeassistant/components/vera/lock.py +++ b/homeassistant/components/vera/lock.py @@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from . import VeraDevice -from .const import DOMAIN +from .common import ControllerData, get_controller_data _LOGGER = logging.getLogger(__name__) @@ -27,10 +27,10 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" - controller_data = hass.data[DOMAIN] + controller_data = get_controller_data(hass, entry) async_add_entities( [ - VeraLock(device, controller_data.controller) + VeraLock(device, controller_data) for device in controller_data.devices.get(PLATFORM_DOMAIN) ] ) @@ -39,10 +39,10 @@ async def async_setup_entry( class VeraLock(VeraDevice, LockEntity): """Representation of a Vera lock.""" - def __init__(self, vera_device, controller): + def __init__(self, vera_device, controller_data: ControllerData): """Initialize the Vera device.""" self._state = None - VeraDevice.__init__(self, vera_device, controller) + VeraDevice.__init__(self, vera_device, controller_data) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) def lock(self, **kwargs): diff --git a/homeassistant/components/vera/scene.py b/homeassistant/components/vera/scene.py index 2f3069f5332..c12f07c15af 100644 --- a/homeassistant/components/vera/scene.py +++ b/homeassistant/components/vera/scene.py @@ -8,7 +8,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -from .const import DOMAIN, VERA_ID_FORMAT +from .common import ControllerData, get_controller_data +from .const import VERA_ID_FORMAT _LOGGER = logging.getLogger(__name__) @@ -19,22 +20,19 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" - controller_data = hass.data[DOMAIN] + controller_data = get_controller_data(hass, entry) async_add_entities( - [ - VeraScene(device, controller_data.controller) - for device in controller_data.scenes - ] + [VeraScene(device, controller_data) for device in controller_data.scenes] ) class VeraScene(Scene): """Representation of a Vera scene entity.""" - def __init__(self, vera_scene, controller): + def __init__(self, vera_scene, controller_data: ControllerData): """Initialize the scene.""" self.vera_scene = vera_scene - self.controller = controller + self.controller = controller_data.controller self._name = self.vera_scene.name # Append device id to prevent name clashes in HA. diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py index 3c4e0974b85..697af6f4562 100644 --- a/homeassistant/components/vera/sensor.py +++ b/homeassistant/components/vera/sensor.py @@ -13,7 +13,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import convert from . import VeraDevice -from .const import DOMAIN +from .common import ControllerData, get_controller_data _LOGGER = logging.getLogger(__name__) @@ -26,10 +26,10 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" - controller_data = hass.data[DOMAIN] + controller_data = get_controller_data(hass, entry) async_add_entities( [ - VeraSensor(device, controller_data.controller) + VeraSensor(device, controller_data) for device in controller_data.devices.get(PLATFORM_DOMAIN) ] ) @@ -38,12 +38,12 @@ async def async_setup_entry( class VeraSensor(VeraDevice, Entity): """Representation of a Vera Sensor.""" - def __init__(self, vera_device, controller): + def __init__(self, vera_device, controller_data: ControllerData): """Initialize the sensor.""" self.current_value = None self._temperature_units = None self.last_changed_time = None - VeraDevice.__init__(self, vera_device, controller) + VeraDevice.__init__(self, vera_device, controller_data) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) @property diff --git a/homeassistant/components/vera/strings.json b/homeassistant/components/vera/strings.json index 7b294eddbb9..844d1777f5d 100644 --- a/homeassistant/components/vera/strings.json +++ b/homeassistant/components/vera/strings.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_configured": "A controller is already configured.", "cannot_connect": "Could not connect to controller with url {base_url}" }, "step": { diff --git a/homeassistant/components/vera/switch.py b/homeassistant/components/vera/switch.py index 0a9a94d6372..9e5af432ce8 100644 --- a/homeassistant/components/vera/switch.py +++ b/homeassistant/components/vera/switch.py @@ -13,7 +13,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import convert from . import VeraDevice -from .const import DOMAIN +from .common import ControllerData, get_controller_data _LOGGER = logging.getLogger(__name__) @@ -24,10 +24,10 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" - controller_data = hass.data[DOMAIN] + controller_data = get_controller_data(hass, entry) async_add_entities( [ - VeraSwitch(device, controller_data.controller) + VeraSwitch(device, controller_data) for device in controller_data.devices.get(PLATFORM_DOMAIN) ] ) @@ -36,10 +36,10 @@ async def async_setup_entry( class VeraSwitch(VeraDevice, SwitchEntity): """Representation of a Vera Switch.""" - def __init__(self, vera_device, controller): + def __init__(self, vera_device, controller_data: ControllerData): """Initialize the Vera device.""" self._state = False - VeraDevice.__init__(self, vera_device, controller) + VeraDevice.__init__(self, vera_device, controller_data) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) def turn_on(self, **kwargs): diff --git a/tests/components/vera/common.py b/tests/components/vera/common.py index 31e7c706ec9..29c6a0e8683 100644 --- a/tests/components/vera/common.py +++ b/tests/components/vera/common.py @@ -1,10 +1,15 @@ """Common code for tests.""" - +from enum import Enum from typing import Callable, Dict, NamedTuple, Tuple import pyvera as pv -from homeassistant.components.vera.const import CONF_CONTROLLER, DOMAIN +from homeassistant import config_entries +from homeassistant.components.vera.const import ( + CONF_CONTROLLER, + CONF_LEGACY_UNIQUE_ID, + DOMAIN, +) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -24,7 +29,15 @@ class ControllerData(NamedTuple): class ComponentData(NamedTuple): """Test data about the vera component.""" - controller_data: ControllerData + controller_data: Tuple[ControllerData] + + +class ConfigSource(Enum): + """Source of configuration.""" + + FILE = "file" + CONFIG_FLOW = "config_flow" + CONFIG_ENTRY = "config_entry" class ControllerConfig(NamedTuple): @@ -32,31 +45,34 @@ class ControllerConfig(NamedTuple): config: Dict options: Dict - config_from_file: bool + config_source: ConfigSource serial_number: str devices: Tuple[pv.VeraDevice, ...] scenes: Tuple[pv.VeraScene, ...] setup_callback: SetupCallback + legacy_entity_unique_id: bool def new_simple_controller_config( config: dict = None, options: dict = None, - config_from_file=False, + config_source=ConfigSource.CONFIG_FLOW, serial_number="1111", devices: Tuple[pv.VeraDevice, ...] = (), scenes: Tuple[pv.VeraScene, ...] = (), setup_callback: SetupCallback = None, + legacy_entity_unique_id=False, ) -> ControllerConfig: """Create simple contorller config.""" return ControllerConfig( config=config or {CONF_CONTROLLER: "http://127.0.0.1:123"}, options=options, - config_from_file=config_from_file, + config_source=config_source, serial_number=serial_number, devices=devices, scenes=scenes, setup_callback=setup_callback, + legacy_entity_unique_id=legacy_entity_unique_id, ) @@ -68,14 +84,38 @@ class ComponentFactory: self.vera_controller_class_mock = vera_controller_class_mock async def configure_component( - self, hass: HomeAssistant, controller_config: ControllerConfig + self, + hass: HomeAssistant, + controller_config: ControllerConfig = None, + controller_configs: Tuple[ControllerConfig] = (), ) -> ComponentData: + """Configure the component with multiple specific mock data.""" + configs = list(controller_configs) + + if controller_config: + configs.append(controller_config) + + return ComponentData( + controller_data=tuple( + [ + await self._configure_component(hass, controller_config) + for controller_config in configs + ] + ) + ) + + async def _configure_component( + self, hass: HomeAssistant, controller_config: ControllerConfig + ) -> ControllerData: """Configure the component with specific mock data.""" component_config = { **(controller_config.config or {}), **(controller_config.options or {}), } + if controller_config.legacy_entity_unique_id: + component_config[CONF_LEGACY_UNIQUE_ID] = True + controller = MagicMock(spec=pv.VeraController) # type: pv.VeraController controller.base_url = component_config.get(CONF_CONTROLLER) controller.register = MagicMock() @@ -101,7 +141,7 @@ class ComponentFactory: hass_config = {} # Setup component through config file import. - if controller_config.config_from_file: + if controller_config.config_source == ConfigSource.FILE: hass_config[DOMAIN] = component_config # Setup Home Assistant. @@ -109,9 +149,21 @@ class ComponentFactory: await hass.async_block_till_done() # Setup component through config flow. - if not controller_config.config_from_file: + if controller_config.config_source == ConfigSource.CONFIG_FLOW: + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=component_config, + ) + await hass.async_block_till_done() + + # Setup component directly from config entry. + if controller_config.config_source == ConfigSource.CONFIG_ENTRY: entry = MockConfigEntry( - domain=DOMAIN, data=component_config, options={}, unique_id="12345" + domain=DOMAIN, + data=controller_config.config, + options=controller_config.options, + unique_id="12345", ) entry.add_to_hass(hass) @@ -124,8 +176,4 @@ class ComponentFactory: else None ) - return ComponentData( - controller_data=ControllerData( - controller=controller, update_callback=update_callback - ) - ) + return ControllerData(controller=controller, update_callback=update_callback) diff --git a/tests/components/vera/test_binary_sensor.py b/tests/components/vera/test_binary_sensor.py index 4b0d41d9a1e..a02c2ef1635 100644 --- a/tests/components/vera/test_binary_sensor.py +++ b/tests/components/vera/test_binary_sensor.py @@ -14,7 +14,7 @@ async def test_binary_sensor( """Test function.""" vera_device = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor vera_device.device_id = 1 - vera_device.vera_device_id = 1 + vera_device.vera_device_id = vera_device.device_id vera_device.name = "dev1" vera_device.is_tripped = False entity_id = "binary_sensor.dev1_1" @@ -23,7 +23,7 @@ async def test_binary_sensor( hass=hass, controller_config=new_simple_controller_config(devices=(vera_device,)), ) - update_callback = component_data.controller_data.update_callback + update_callback = component_data.controller_data[0].update_callback vera_device.is_tripped = False update_callback(vera_device) diff --git a/tests/components/vera/test_climate.py b/tests/components/vera/test_climate.py index f11f3ea5a3b..370ecc18dcd 100644 --- a/tests/components/vera/test_climate.py +++ b/tests/components/vera/test_climate.py @@ -22,6 +22,7 @@ async def test_climate( """Test function.""" vera_device = MagicMock(spec=pv.VeraThermostat) # type: pv.VeraThermostat vera_device.device_id = 1 + vera_device.vera_device_id = vera_device.device_id vera_device.name = "dev1" vera_device.category = pv.CATEGORY_THERMOSTAT vera_device.power = 10 @@ -34,7 +35,7 @@ async def test_climate( hass=hass, controller_config=new_simple_controller_config(devices=(vera_device,)), ) - update_callback = component_data.controller_data.update_callback + update_callback = component_data.controller_data[0].update_callback assert hass.states.get(entity_id).state == HVAC_MODE_OFF @@ -131,6 +132,7 @@ async def test_climate_f( """Test function.""" vera_device = MagicMock(spec=pv.VeraThermostat) # type: pv.VeraThermostat vera_device.device_id = 1 + vera_device.vera_device_id = vera_device.device_id vera_device.name = "dev1" vera_device.category = pv.CATEGORY_THERMOSTAT vera_device.power = 10 @@ -148,7 +150,7 @@ async def test_climate_f( devices=(vera_device,), setup_callback=setup_callback ), ) - update_callback = component_data.controller_data.update_callback + update_callback = component_data.controller_data[0].update_callback await hass.services.async_call( "climate", diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index 793e313125c..dceac728e4d 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -2,17 +2,13 @@ from requests.exceptions import RequestException from homeassistant import config_entries, data_entry_flow -from homeassistant.components.vera import CONF_CONTROLLER, DOMAIN +from homeassistant.components.vera import CONF_CONTROLLER, CONF_LEGACY_UNIQUE_ID, DOMAIN from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM from tests.async_mock import MagicMock, patch -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, mock_registry async def test_async_step_user_success(hass: HomeAssistant) -> None: @@ -44,6 +40,7 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None: CONF_SOURCE: config_entries.SOURCE_USER, CONF_LIGHTS: [12, 13], CONF_EXCLUDE: [14, 15], + CONF_LEGACY_UNIQUE_ID: False, } assert result["result"].unique_id == controller.serial_number @@ -51,18 +48,6 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None: assert entries -async def test_async_step_user_already_configured(hass: HomeAssistant) -> None: - """Test user step with entry already configured.""" - entry = MockConfigEntry(domain=DOMAIN, data={}, options={}, unique_id="12345") - entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - async def test_async_step_import_success(hass: HomeAssistant) -> None: """Test import step success.""" with patch("pyvera.VeraController") as vera_controller_class_mock: @@ -82,28 +67,40 @@ async def test_async_step_import_success(hass: HomeAssistant) -> None: assert result["data"] == { CONF_CONTROLLER: "http://127.0.0.1:123", CONF_SOURCE: config_entries.SOURCE_IMPORT, + CONF_LEGACY_UNIQUE_ID: False, } assert result["result"].unique_id == controller.serial_number -async def test_async_step_import_alredy_setup(hass: HomeAssistant) -> None: - """Test import step with entry already setup.""" - entry = MockConfigEntry(domain=DOMAIN, data={}, options={}, unique_id="12345") - entry.add_to_hass(hass) +async def test_async_step_import_success_with_legacy_unique_id( + hass: HomeAssistant, +) -> None: + """Test import step success with legacy unique id.""" + entity_registry = mock_registry(hass) + entity_registry.async_get_or_create( + domain="switch", platform=DOMAIN, unique_id="12" + ) with patch("pyvera.VeraController") as vera_controller_class_mock: controller = MagicMock() controller.refresh_data = MagicMock() - controller.serial_number = "12345" + controller.serial_number = "serial_number_1" vera_controller_class_mock.return_value = controller result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_CONTROLLER: "http://localhost:445"}, + data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, ) - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "http://127.0.0.1:123" + assert result["data"] == { + CONF_CONTROLLER: "http://127.0.0.1:123", + CONF_SOURCE: config_entries.SOURCE_IMPORT, + CONF_LEGACY_UNIQUE_ID: True, + } + assert result["result"].unique_id == controller.serial_number async def test_async_step_finish_error(hass: HomeAssistant) -> None: diff --git a/tests/components/vera/test_cover.py b/tests/components/vera/test_cover.py index 311c8013d86..f3dc2263749 100644 --- a/tests/components/vera/test_cover.py +++ b/tests/components/vera/test_cover.py @@ -14,6 +14,7 @@ async def test_cover( """Test function.""" vera_device = MagicMock(spec=pv.VeraCurtain) # type: pv.VeraCurtain vera_device.device_id = 1 + vera_device.vera_device_id = vera_device.device_id vera_device.name = "dev1" vera_device.category = pv.CATEGORY_CURTAIN vera_device.is_closed = False @@ -24,7 +25,7 @@ async def test_cover( hass=hass, controller_config=new_simple_controller_config(devices=(vera_device,)), ) - update_callback = component_data.controller_data.update_callback + update_callback = component_data.controller_data[0].update_callback assert hass.states.get(entity_id).state == "closed" assert hass.states.get(entity_id).attributes["current_position"] == 0 diff --git a/tests/components/vera/test_init.py b/tests/components/vera/test_init.py index 210037a2ca3..b3f7b3249ef 100644 --- a/tests/components/vera/test_init.py +++ b/tests/components/vera/test_init.py @@ -12,10 +12,10 @@ from homeassistant.components.vera import ( from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED from homeassistant.core import HomeAssistant -from .common import ComponentFactory, new_simple_controller_config +from .common import ComponentFactory, ConfigSource, new_simple_controller_config from tests.async_mock import MagicMock -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, mock_registry async def test_init( @@ -24,7 +24,7 @@ async def test_init( """Test function.""" vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor vera_device1.device_id = 1 - vera_device1.vera_device_id = 1 + vera_device1.vera_device_id = vera_device1.device_id vera_device1.name = "first_dev" vera_device1.is_tripped = False entity1_id = "binary_sensor.first_dev_1" @@ -33,7 +33,7 @@ async def test_init( hass=hass, controller_config=new_simple_controller_config( config={CONF_CONTROLLER: "http://127.0.0.1:111"}, - config_from_file=False, + config_source=ConfigSource.CONFIG_FLOW, serial_number="first_serial", devices=(vera_device1,), ), @@ -41,8 +41,8 @@ async def test_init( entity_registry = await hass.helpers.entity_registry.async_get_registry() entry1 = entity_registry.async_get(entity1_id) - assert entry1 + assert entry1.unique_id == "vera_first_serial_1" async def test_init_from_file( @@ -51,7 +51,7 @@ async def test_init_from_file( """Test function.""" vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor vera_device1.device_id = 1 - vera_device1.vera_device_id = 1 + vera_device1.vera_device_id = vera_device1.device_id vera_device1.name = "first_dev" vera_device1.is_tripped = False entity1_id = "binary_sensor.first_dev_1" @@ -60,7 +60,7 @@ async def test_init_from_file( hass=hass, controller_config=new_simple_controller_config( config={CONF_CONTROLLER: "http://127.0.0.1:111"}, - config_from_file=True, + config_source=ConfigSource.FILE, serial_number="first_serial", devices=(vera_device1,), ), @@ -69,6 +69,62 @@ async def test_init_from_file( entity_registry = await hass.helpers.entity_registry.async_get_registry() entry1 = entity_registry.async_get(entity1_id) assert entry1 + assert entry1.unique_id == "vera_first_serial_1" + + +async def test_multiple_controllers_with_legacy_one( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test multiple controllers with one legacy controller.""" + vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor + vera_device1.device_id = 1 + vera_device1.vera_device_id = vera_device1.device_id + vera_device1.name = "first_dev" + vera_device1.is_tripped = False + entity1_id = "binary_sensor.first_dev_1" + + vera_device2 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor + vera_device2.device_id = 2 + vera_device2.vera_device_id = vera_device2.device_id + vera_device2.name = "second_dev" + vera_device2.is_tripped = False + entity2_id = "binary_sensor.second_dev_2" + + # Add existing entity registry entry from previous setup. + entity_registry = mock_registry(hass) + entity_registry.async_get_or_create( + domain="switch", platform=DOMAIN, unique_id="12" + ) + + await vera_component_factory.configure_component( + hass=hass, + controller_config=new_simple_controller_config( + config={CONF_CONTROLLER: "http://127.0.0.1:111"}, + config_source=ConfigSource.FILE, + serial_number="first_serial", + devices=(vera_device1,), + ), + ) + + await vera_component_factory.configure_component( + hass=hass, + controller_config=new_simple_controller_config( + config={CONF_CONTROLLER: "http://127.0.0.1:222"}, + config_source=ConfigSource.CONFIG_FLOW, + serial_number="second_serial", + devices=(vera_device2,), + ), + ) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entry1 = entity_registry.async_get(entity1_id) + assert entry1 + assert entry1.unique_id == "1" + + entry2 = entity_registry.async_get(entity2_id) + assert entry2 + assert entry2.unique_id == "vera_second_serial_2" async def test_unload( @@ -77,7 +133,7 @@ async def test_unload( """Test function.""" vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor vera_device1.device_id = 1 - vera_device1.vera_device_id = 1 + vera_device1.vera_device_id = vera_device1.device_id vera_device1.name = "first_dev" vera_device1.is_tripped = False @@ -145,6 +201,7 @@ async def test_exclude_and_light_ids( vera_device3 = MagicMock(spec=pv.VeraSwitch) # type: pv.VeraSwitch vera_device3.device_id = 3 + vera_device3.vera_device_id = 3 vera_device3.name = "dev3" vera_device3.category = pv.CATEGORY_SWITCH vera_device3.is_switched_on = MagicMock(return_value=False) @@ -152,6 +209,7 @@ async def test_exclude_and_light_ids( vera_device4 = MagicMock(spec=pv.VeraSwitch) # type: pv.VeraSwitch vera_device4.device_id = 4 + vera_device4.vera_device_id = 4 vera_device4.name = "dev4" vera_device4.category = pv.CATEGORY_SWITCH vera_device4.is_switched_on = MagicMock(return_value=False) @@ -160,6 +218,7 @@ async def test_exclude_and_light_ids( component_data = await vera_component_factory.configure_component( hass=hass, controller_config=new_simple_controller_config( + config_source=ConfigSource.CONFIG_ENTRY, devices=(vera_device1, vera_device2, vera_device3, vera_device4), config={**{CONF_CONTROLLER: "http://127.0.0.1:123"}, **options}, ), @@ -167,12 +226,10 @@ async def test_exclude_and_light_ids( # Assert the entries were setup correctly. config_entry = next(iter(hass.config_entries.async_entries(DOMAIN))) - assert config_entry.options == { - CONF_LIGHTS: [4, 10, 12], - CONF_EXCLUDE: [1], - } + assert config_entry.options[CONF_LIGHTS] == [4, 10, 12] + assert config_entry.options[CONF_EXCLUDE] == [1] - update_callback = component_data.controller_data.update_callback + update_callback = component_data.controller_data[0].update_callback update_callback(vera_device1) update_callback(vera_device2) diff --git a/tests/components/vera/test_light.py b/tests/components/vera/test_light.py index 99391d8d82a..72118e33a31 100644 --- a/tests/components/vera/test_light.py +++ b/tests/components/vera/test_light.py @@ -15,6 +15,7 @@ async def test_light( """Test function.""" vera_device = MagicMock(spec=pv.VeraDimmer) # type: pv.VeraDimmer vera_device.device_id = 1 + vera_device.vera_device_id = vera_device.device_id vera_device.name = "dev1" vera_device.category = pv.CATEGORY_DIMMER vera_device.is_switched_on = MagicMock(return_value=False) @@ -27,7 +28,7 @@ async def test_light( hass=hass, controller_config=new_simple_controller_config(devices=(vera_device,)), ) - update_callback = component_data.controller_data.update_callback + update_callback = component_data.controller_data[0].update_callback assert hass.states.get(entity_id).state == "off" diff --git a/tests/components/vera/test_lock.py b/tests/components/vera/test_lock.py index 11af1f5a7b7..b3433b2bafb 100644 --- a/tests/components/vera/test_lock.py +++ b/tests/components/vera/test_lock.py @@ -15,6 +15,7 @@ async def test_lock( """Test function.""" vera_device = MagicMock(spec=pv.VeraLock) # type: pv.VeraLock vera_device.device_id = 1 + vera_device.vera_device_id = vera_device.device_id vera_device.name = "dev1" vera_device.category = pv.CATEGORY_LOCK vera_device.is_locked = MagicMock(return_value=False) @@ -24,7 +25,7 @@ async def test_lock( hass=hass, controller_config=new_simple_controller_config(devices=(vera_device,)), ) - update_callback = component_data.controller_data.update_callback + update_callback = component_data.controller_data[0].update_callback assert hass.states.get(entity_id).state == STATE_UNLOCKED diff --git a/tests/components/vera/test_scene.py b/tests/components/vera/test_scene.py index 29ef338b9f1..6c80f27d8c8 100644 --- a/tests/components/vera/test_scene.py +++ b/tests/components/vera/test_scene.py @@ -14,6 +14,7 @@ async def test_scene( """Test function.""" vera_scene = MagicMock(spec=pv.VeraScene) # type: pv.VeraScene vera_scene.scene_id = 1 + vera_scene.vera_scene_id = vera_scene.scene_id vera_scene.name = "dev1" entity_id = "scene.dev1_1" diff --git a/tests/components/vera/test_sensor.py b/tests/components/vera/test_sensor.py index 36730e8d6d2..58e91a5581b 100644 --- a/tests/components/vera/test_sensor.py +++ b/tests/components/vera/test_sensor.py @@ -23,6 +23,7 @@ async def run_sensor_test( """Test generic sensor.""" vera_device = MagicMock(spec=pv.VeraSensor) # type: pv.VeraSensor vera_device.device_id = 1 + vera_device.vera_device_id = vera_device.device_id vera_device.name = "dev1" vera_device.category = category setattr(vera_device, class_property, "33") @@ -34,7 +35,7 @@ async def run_sensor_test( devices=(vera_device,), setup_callback=setup_callback ), ) - update_callback = component_data.controller_data.update_callback + update_callback = component_data.controller_data[0].update_callback for (initial_value, state_value) in assert_states: setattr(vera_device, class_property, initial_value) @@ -175,6 +176,7 @@ async def test_scene_controller_sensor( """Test function.""" vera_device = MagicMock(spec=pv.VeraSensor) # type: pv.VeraSensor vera_device.device_id = 1 + vera_device.vera_device_id = vera_device.device_id vera_device.name = "dev1" vera_device.category = pv.CATEGORY_SCENE_CONTROLLER vera_device.get_last_scene_id = MagicMock(return_value="id0") @@ -185,7 +187,7 @@ async def test_scene_controller_sensor( hass=hass, controller_config=new_simple_controller_config(devices=(vera_device,)), ) - update_callback = component_data.controller_data.update_callback + update_callback = component_data.controller_data[0].update_callback vera_device.get_last_scene_time.return_value = "1111" update_callback(vera_device) diff --git a/tests/components/vera/test_switch.py b/tests/components/vera/test_switch.py index 60e31add4bd..42c74e4e843 100644 --- a/tests/components/vera/test_switch.py +++ b/tests/components/vera/test_switch.py @@ -14,6 +14,7 @@ async def test_switch( """Test function.""" vera_device = MagicMock(spec=pv.VeraSwitch) # type: pv.VeraSwitch vera_device.device_id = 1 + vera_device.vera_device_id = vera_device.device_id vera_device.name = "dev1" vera_device.category = pv.CATEGORY_SWITCH vera_device.is_switched_on = MagicMock(return_value=False) @@ -21,9 +22,11 @@ async def test_switch( component_data = await vera_component_factory.configure_component( hass=hass, - controller_config=new_simple_controller_config(devices=(vera_device,)), + controller_config=new_simple_controller_config( + devices=(vera_device,), legacy_entity_unique_id=False + ), ) - update_callback = component_data.controller_data.update_callback + update_callback = component_data.controller_data[0].update_callback assert hass.states.get(entity_id).state == "off"