From 15f28a1a2aaefa9f0bb2c9a26a801881eb12d0a7 Mon Sep 17 00:00:00 2001 From: Robbie Van Gorkom Date: Thu, 12 Dec 2019 07:25:41 -0800 Subject: [PATCH 01/21] Adding vera config entries support. --- homeassistant/components/vera/__init__.py | 222 ++++---------- .../components/vera/binary_sensor.py | 32 +- homeassistant/components/vera/climate.py | 32 +- homeassistant/components/vera/common.py | 286 ++++++++++++++++++ homeassistant/components/vera/config_flow.py | 13 + homeassistant/components/vera/const.py | 11 + homeassistant/components/vera/cover.py | 33 +- homeassistant/components/vera/light.py | 27 +- homeassistant/components/vera/lock.py | 34 ++- homeassistant/components/vera/scene.py | 29 +- homeassistant/components/vera/sensor.py | 29 +- homeassistant/components/vera/strings.json | 5 + homeassistant/components/vera/switch.py | 32 +- tests/components/vera/common.py | 97 ++++-- tests/components/vera/conftest.py | 4 +- tests/components/vera/test_binary_sensor.py | 12 +- tests/components/vera/test_climate.py | 16 +- tests/components/vera/test_cover.py | 8 +- tests/components/vera/test_init.py | 99 +++--- tests/components/vera/test_light.py | 8 +- tests/components/vera/test_lock.py | 8 +- tests/components/vera/test_scene.py | 4 +- tests/components/vera/test_sensor.py | 16 +- tests/components/vera/test_switch.py | 8 +- 24 files changed, 686 insertions(+), 379 deletions(-) create mode 100644 homeassistant/components/vera/common.py create mode 100644 homeassistant/components/vera/config_flow.py create mode 100644 homeassistant/components/vera/const.py create mode 100644 homeassistant/components/vera/strings.json diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 1c9d412d974ddd..eb44d827382c23 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -1,41 +1,27 @@ """Support for Vera devices.""" -from collections import defaultdict import logging -import pyvera as veraApi from requests.exceptions import RequestException import voluptuous as vol -from homeassistant.const import ( - ATTR_ARMED, - ATTR_BATTERY_LEVEL, - ATTR_LAST_TRIP_TIME, - ATTR_TRIPPED, - CONF_EXCLUDE, - CONF_LIGHTS, - EVENT_HOMEASSISTANT_STOP, +from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv + +from .common import ( + get_configured_platforms, + get_controller_data_by_config, + initialize_controller, + set_controller_data, ) -from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.helpers.entity import Entity -from homeassistant.util import convert, slugify -from homeassistant.util.dt import utc_from_timestamp +from .const import CONF_CONTROLLER, DOMAIN _LOGGER = logging.getLogger(__name__) -DOMAIN = "vera" - VERA_CONTROLLER = "vera_controller" -CONF_CONTROLLER = "vera_controller_url" - -VERA_ID_FORMAT = "{}_{}" - -ATTR_CURRENT_POWER_W = "current_power_w" -ATTR_CURRENT_ENERGY_KWH = "current_energy_kwh" - -VERA_DEVICES = "vera_devices" -VERA_SCENES = "vera_scenes" - VERA_ID_LIST_SCHEMA = vol.Schema([int]) CONFIG_SCHEMA = vol.Schema( @@ -51,164 +37,68 @@ extra=vol.ALLOW_EXTRA, ) -VERA_COMPONENTS = [ - "binary_sensor", - "sensor", - "light", - "switch", - "lock", - "climate", - "cover", - "scene", -] - -def setup(hass, base_config): - """Set up for Vera devices.""" +def setup(hass: HomeAssistant, base_config: dict) -> bool: + """Set up for Vera controllers.""" + config = base_config.get(DOMAIN, []) - def stop_subscription(event): - """Shutdown Vera subscriptions and subscription thread on exit.""" - _LOGGER.info("Shutting down subscriptions") - hass.data[VERA_CONTROLLER].stop() + # Normalize the base url. + config[CONF_CONTROLLER] = config.get(CONF_CONTROLLER).rstrip("/") - config = base_config.get(DOMAIN) + # Build a map of already configured controllers. + base_url_entries_map = {} + for config_entry in hass.config_entries.async_entries(DOMAIN): + base_url = config_entry.data.get(CONF_CONTROLLER) + base_url_entries_map[base_url] = config_entry - # Get Vera specific configuration. base_url = config.get(CONF_CONTROLLER) - light_ids = config.get(CONF_LIGHTS) - exclude_ids = config.get(CONF_EXCLUDE) - # Initialize the Vera controller. - controller, _ = veraApi.init_controller(base_url) - hass.data[VERA_CONTROLLER] = controller - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription) + entry = base_url_entries_map.get(base_url) + if entry: + _LOGGER.debug("Updating existing config for %s", base_url) + hass.config_entries.async_update_entry(entry=entry, data=config) + return True - try: - all_devices = controller.get_devices() + _LOGGER.debug("Creating new config for %s", base_url) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config, + ) + ) - all_scenes = controller.get_scenes() + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Do setup of vera.""" + try: + controller_data = initialize_controller(hass, config_entry.data) except RequestException: # There was a network related error connecting to the Vera controller. _LOGGER.exception("Error communicating with Vera API") return False - # Exclude devices unwanted by user. - devices = [device for device in all_devices if device.device_id not in exclude_ids] - - 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) - hass.data[VERA_DEVICES] = vera_devices - - vera_scenes = [] - for scene in all_scenes: - vera_scenes.append(scene) - hass.data[VERA_SCENES] = vera_scenes + set_controller_data(hass, controller_data) - for component in VERA_COMPONENTS: - discovery.load_platform(hass, component, DOMAIN, {}, base_config) - - return True - - -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: - return "light" - return "switch" - return None - - -class VeraDevice(Entity): - """Representation of a Vera device entity.""" - - def __init__(self, vera_device, controller): - """Initialize the device.""" - self.vera_device = vera_device - self.controller = 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 + # Forward the config data to the necessary platforms. + for platform in get_configured_platforms(controller_data): + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) - self.controller.register(vera_device, self._update_callback) - - def _update_callback(self, _device): - """Update the state.""" - self.schedule_update_ha_state(True) - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def should_poll(self): - """Get polling requirement from vera device.""" - return self.vera_device.should_poll - - @property - def device_state_attributes(self): - """Return the state attributes of the device.""" - attr = {} - - if self.vera_device.has_battery: - attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level - - if self.vera_device.is_armable: - armed = self.vera_device.is_armed - attr[ATTR_ARMED] = "True" if armed else "False" - - if self.vera_device.is_trippable: - last_tripped = self.vera_device.last_trip - if last_tripped is not None: - utc_time = utc_from_timestamp(int(last_tripped)) - attr[ATTR_LAST_TRIP_TIME] = utc_time.isoformat() - else: - attr[ATTR_LAST_TRIP_TIME] = None - tripped = self.vera_device.is_tripped - attr[ATTR_TRIPPED] = "True" if tripped else "False" - - power = self.vera_device.power - if power: - attr[ATTR_CURRENT_POWER_W] = convert(power, float, 0.0) + return True - energy = self.vera_device.energy - if energy: - attr[ATTR_CURRENT_ENERGY_KWH] = convert(energy, float, 0.0) - attr["Vera Device Id"] = self.vera_device.vera_device_id +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload Withings config entry.""" + controller_data = get_controller_data_by_config(hass=hass, entry=config_entry) - return attr + if not controller_data: + return True - @property - def unique_id(self) -> str: - """Return a unique ID. + for platform in get_configured_platforms(controller_data): + hass.async_create_task( + hass.config_entries.async_forward_entry_unload(config_entry, platform) + ) - The Vera assigns a unique and immutable ID number to each device. - """ - return str(self.vera_device.vera_device_id) + return True diff --git a/homeassistant/components/vera/binary_sensor.py b/homeassistant/components/vera/binary_sensor.py index 061d2c5c99aef4..ec693c3ada67ec 100644 --- a/homeassistant/components/vera/binary_sensor.py +++ b/homeassistant/components/vera/binary_sensor.py @@ -1,21 +1,33 @@ """Support for Vera binary sensors.""" import logging +from typing import Callable, List -from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySensorDevice +from homeassistant.components.binary_sensor import ( + DOMAIN as PLATFORM_DOMAIN, + ENTITY_ID_FORMAT, + BinarySensorDevice, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity -from . import VERA_CONTROLLER, VERA_DEVICES, VeraDevice +from .common import VeraDevice, setup_device_entities _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Perform the setup for Vera controller devices.""" - add_entities( - [ - VeraBinarySensor(device, hass.data[VERA_CONTROLLER]) - for device in hass.data[VERA_DEVICES]["binary_sensor"] - ], - True, +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up the sensor config entry.""" + setup_device_entities( + hass=hass, + entry=entry, + async_add_entities=async_add_entities, + platform=PLATFORM_DOMAIN, + generator=VeraBinarySensor, ) diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index 60e73d48978cdb..e3dd9a64af71cd 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -1,7 +1,12 @@ """Support for Vera thermostats.""" import logging +from typing import Callable, List -from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateDevice +from homeassistant.components.climate import ( + DOMAIN as PLATFORM_DOMAIN, + ENTITY_ID_FORMAT, + ClimateDevice, +) from homeassistant.components.climate.const import ( FAN_AUTO, FAN_ON, @@ -12,10 +17,13 @@ SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity from homeassistant.util import convert -from . import VERA_CONTROLLER, VERA_DEVICES, VeraDevice +from .common import VeraDevice, setup_device_entities _LOGGER = logging.getLogger(__name__) @@ -25,14 +33,18 @@ SUPPORT_HVAC = [HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF] -def setup_platform(hass, config, add_entities_callback, discovery_info=None): - """Set up of Vera thermostats.""" - add_entities_callback( - [ - VeraThermostat(device, hass.data[VERA_CONTROLLER]) - for device in hass.data[VERA_DEVICES]["climate"] - ], - True, +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up the sensor config entry.""" + setup_device_entities( + hass=hass, + entry=entry, + async_add_entities=async_add_entities, + platform=PLATFORM_DOMAIN, + generator=VeraThermostat, ) diff --git a/homeassistant/components/vera/common.py b/homeassistant/components/vera/common.py new file mode 100644 index 00000000000000..9e5e8713665c36 --- /dev/null +++ b/homeassistant/components/vera/common.py @@ -0,0 +1,286 @@ +"""Common vera code.""" +from collections import defaultdict +import logging +from typing import Callable, DefaultDict, List, NamedTuple, Optional, Union + +import pyvera as pv + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN +from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, Scene +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ARMED, + ATTR_BATTERY_LEVEL, + ATTR_LAST_TRIP_TIME, + ATTR_TRIPPED, + CONF_EXCLUDE, + CONF_LIGHTS, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity +from homeassistant.util import convert, slugify +from homeassistant.util.dt import utc_from_timestamp + +from .const import ( + ATTR_CURRENT_ENERGY_KWH, + ATTR_CURRENT_POWER_W, + CONF_CONTROLLER, + CONTROLLER_DATAS, + DOMAIN, + VERA_ID_FORMAT, +) + +_LOGGER = logging.getLogger(__name__) + + +ControllerData = NamedTuple( + "ControllerData", + ( + ("controller", pv.VeraController), + ("devices", DefaultDict[str, List[pv.VeraDevice]]), + ("scenes", List[pv.VeraScene]), + ), +) + + +class VeraDevice(Entity): + """Representation of a Vera device entity.""" + + def __init__(self, vera_device: pv.VeraDevice, controller: pv.VeraController): + """Initialize the device.""" + self.vera_device = vera_device + self.controller = 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 + ) + + self.controller.register(vera_device, self._update_callback) + + def _update_callback(self, _device): + """Update the state.""" + self.schedule_update_ha_state(True) + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def should_poll(self): + """Get polling requirement from vera device.""" + return self.vera_device.should_poll + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + attr = {} + + if self.vera_device.has_battery: + attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + + if self.vera_device.is_armable: + armed = self.vera_device.is_armed + attr[ATTR_ARMED] = "True" if armed else "False" + + if self.vera_device.is_trippable: + last_tripped = self.vera_device.last_trip + if last_tripped is not None: + utc_time = utc_from_timestamp(int(last_tripped)) + attr[ATTR_LAST_TRIP_TIME] = utc_time.isoformat() + else: + attr[ATTR_LAST_TRIP_TIME] = None + tripped = self.vera_device.is_tripped + attr[ATTR_TRIPPED] = "True" if tripped else "False" + + power = self.vera_device.power + if power: + attr[ATTR_CURRENT_POWER_W] = convert(power, float, 0.0) + + energy = self.vera_device.energy + if energy: + attr[ATTR_CURRENT_ENERGY_KWH] = convert(energy, float, 0.0) + + attr["Vera Device Id"] = self.vera_device.vera_device_id + + return attr + + @property + def unique_id(self) -> str: + """Return a unique ID. + + The Vera assigns a unique and immutable ID number to each device. + """ + return str(self.vera_device.vera_device_id) + + +def initialize_controller(hass: HomeAssistant, config: dict) -> ControllerData: + """Initialize a controller.""" + # Get Vera specific configuration. + base_url = config.get(CONF_CONTROLLER) + light_ids = config.get(CONF_LIGHTS) + exclude_ids = config.get(CONF_EXCLUDE) + + # Initialize the Vera controller. + controller = pv.VeraController(base_url) + controller.start() + + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, lambda event: controller.stop() + ) + + controller.refresh_data() + all_devices = controller.get_devices() + all_scenes = controller.get_scenes() + # Exclude devices unwanted by user. + devices = [device for device in all_devices if device.device_id not in exclude_ids] + + 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) + + vera_scenes = [] + for scene in all_scenes: + vera_scenes.append(scene) + + return ControllerData( + controller=controller, devices=vera_devices, scenes=vera_scenes + ) + + +EntityDeviceGenerator = Callable[[pv.VeraDevice, pv.VeraController], Entity] +EntitySceneGenerator = Callable[[pv.VeraDevice, pv.VeraController], Scene] +EntityGenerator = Union[EntityDeviceGenerator, EntitySceneGenerator] +ItemCollector = Callable[ + [ControllerData, str], List[Union[pv.VeraDevice, pv.VeraScene]] +] + + +def setup_device_entities( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], + platform: str, + generator: EntityDeviceGenerator, +): + """Create and add vera entities for devices in a platform.""" + + _setup_entities( + hass=hass, + entry=entry, + async_add_entities=async_add_entities, + platform=platform, + generator=generator, + item_collector=lambda controller_data, platform: controller_data.devices.get( + platform + ), + ) + + +def setup_scene_entities( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], + platform: str, + generator: EntitySceneGenerator, +): + """Create and add vera scenes.""" + _setup_entities( + hass=hass, + entry=entry, + async_add_entities=async_add_entities, + platform=platform, + generator=generator, + item_collector=lambda controller_data, platform: controller_data.scenes, + ) + + +def _setup_entities( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], + platform: str, + generator: EntityGenerator, + item_collector: ItemCollector, +) -> None: + """Create and add vera entities for a given platform.""" + controller_data = get_controller_data_by_config(hass=hass, entry=entry) + + entities = [] + items = item_collector(controller_data, platform) + + for item in items or []: + entities.append(generator(item, controller_data.controller)) + + async_add_entities(entities, True) + + +def map_vera_device(vera_device, remap): + """Map vera classes to Home Assistant types.""" + + if isinstance(vera_device, pv.VeraDimmer): + return LIGHT_DOMAIN + if isinstance(vera_device, pv.VeraBinarySensor): + return BINARY_SENSOR_DOMAIN + if isinstance(vera_device, pv.VeraSensor): + return SENSOR_DOMAIN + if isinstance(vera_device, pv.VeraArmableDevice): + return SWITCH_DOMAIN + if isinstance(vera_device, pv.VeraLock): + return LOCK_DOMAIN + if isinstance(vera_device, pv.VeraThermostat): + return CLIMATE_DOMAIN + if isinstance(vera_device, pv.VeraCurtain): + return COVER_DOMAIN + if isinstance(vera_device, pv.VeraSceneController): + return SENSOR_DOMAIN + if isinstance(vera_device, pv.VeraSwitch): + if vera_device.device_id in remap: + return LIGHT_DOMAIN + return SWITCH_DOMAIN + return None + + +def get_controller_data_by_config( + hass: HomeAssistant, entry: ConfigEntry +) -> Optional[ControllerData]: + """Get controller data from hass data.""" + base_url = entry.data.get(CONF_CONTROLLER) + for controller_data in hass.data[DOMAIN][CONTROLLER_DATAS].values(): + if controller_data.controller.base_url == base_url: + return controller_data + + return None + + +def set_controller_data(hass: HomeAssistant, controller_data: ControllerData) -> None: + """Set controller data in hass data.""" + serial_number = controller_data.controller.serial_number + hass.data[DOMAIN] = hass.data.get(DOMAIN, {}) + hass.data[DOMAIN][CONTROLLER_DATAS] = hass.data[DOMAIN].get(CONTROLLER_DATAS, {}) + hass.data[DOMAIN][CONTROLLER_DATAS][serial_number] = controller_data + + +def get_configured_platforms(controller_data: ControllerData) -> List[str]: + """Get configured platforms for a controller.""" + platforms = [] + for platform in controller_data.devices.keys(): + platforms.append(platform) + + if controller_data.scenes: + platforms.append(SCENE_DOMAIN) + + return set(platforms) diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py new file mode 100644 index 00000000000000..1065e8f8763d2d --- /dev/null +++ b/homeassistant/components/vera/config_flow.py @@ -0,0 +1,13 @@ +"""Config flow for Vera.""" +from homeassistant import config_entries + +from .const import CONF_CONTROLLER, DOMAIN + + +@config_entries.HANDLERS.register(DOMAIN) +class VeraFlowHandler(config_entries.ConfigFlow): + """Vera config flow.""" + + async def async_step_import(self, config: dict): + """Handle a flow initialized by import.""" + return self.async_create_entry(title=config.get(CONF_CONTROLLER), data=config) diff --git a/homeassistant/components/vera/const.py b/homeassistant/components/vera/const.py new file mode 100644 index 00000000000000..c4f1d0efa3a0b2 --- /dev/null +++ b/homeassistant/components/vera/const.py @@ -0,0 +1,11 @@ +"""Vera constants.""" +DOMAIN = "vera" + +CONF_CONTROLLER = "vera_controller_url" + +VERA_ID_FORMAT = "{}_{}" + +ATTR_CURRENT_POWER_W = "current_power_w" +ATTR_CURRENT_ENERGY_KWH = "current_energy_kwh" + +CONTROLLER_DATAS = "controller_datas" diff --git a/homeassistant/components/vera/cover.py b/homeassistant/components/vera/cover.py index b90dd8a053112d..b11d680af369e0 100644 --- a/homeassistant/components/vera/cover.py +++ b/homeassistant/components/vera/cover.py @@ -1,21 +1,34 @@ """Support for Vera cover - curtains, rollershutters etc.""" import logging +from typing import Callable, List -from homeassistant.components.cover import ATTR_POSITION, ENTITY_ID_FORMAT, CoverDevice +from homeassistant.components.cover import ( + ATTR_POSITION, + DOMAIN as PLATFORM_DOMAIN, + ENTITY_ID_FORMAT, + CoverDevice, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity -from . import VERA_CONTROLLER, VERA_DEVICES, VeraDevice +from .common import VeraDevice, setup_device_entities _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Vera covers.""" - add_entities( - [ - VeraCover(device, hass.data[VERA_CONTROLLER]) - for device in hass.data[VERA_DEVICES]["cover"] - ], - True, +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up the sensor config entry.""" + setup_device_entities( + hass=hass, + entry=entry, + async_add_entities=async_add_entities, + platform=PLATFORM_DOMAIN, + generator=VeraCover, ) diff --git a/homeassistant/components/vera/light.py b/homeassistant/components/vera/light.py index fee992356816e3..696ac163f56829 100644 --- a/homeassistant/components/vera/light.py +++ b/homeassistant/components/vera/light.py @@ -1,29 +1,38 @@ """Support for Vera lights.""" import logging +from typing import Callable, List from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, + DOMAIN as PLATFORM_DOMAIN, ENTITY_ID_FORMAT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity import homeassistant.util.color as color_util -from . import VERA_CONTROLLER, VERA_DEVICES, VeraDevice +from .common import VeraDevice, setup_device_entities _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Vera lights.""" - add_entities( - [ - VeraLight(device, hass.data[VERA_CONTROLLER]) - for device in hass.data[VERA_DEVICES]["light"] - ], - True, +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up the sensor config entry.""" + setup_device_entities( + hass=hass, + entry=entry, + async_add_entities=async_add_entities, + platform=PLATFORM_DOMAIN, + generator=VeraLight, ) diff --git a/homeassistant/components/vera/lock.py b/homeassistant/components/vera/lock.py index 23b62bb0331466..e0dacbdedd3caf 100644 --- a/homeassistant/components/vera/lock.py +++ b/homeassistant/components/vera/lock.py @@ -1,10 +1,18 @@ """Support for Vera locks.""" import logging - -from homeassistant.components.lock import ENTITY_ID_FORMAT, LockDevice +from typing import Callable, List + +from homeassistant.components.lock import ( + DOMAIN as PLATFORM_DOMAIN, + ENTITY_ID_FORMAT, + LockDevice, +) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity -from . import VERA_CONTROLLER, VERA_DEVICES, VeraDevice +from .common import VeraDevice, setup_device_entities _LOGGER = logging.getLogger(__name__) @@ -12,14 +20,18 @@ ATTR_LOW_BATTERY = "low_battery" -def setup_platform(hass, config, add_entities, discovery_info=None): - """Find and return Vera locks.""" - add_entities( - [ - VeraLock(device, hass.data[VERA_CONTROLLER]) - for device in hass.data[VERA_DEVICES]["lock"] - ], - True, +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up the sensor config entry.""" + setup_device_entities( + hass=hass, + entry=entry, + async_add_entities=async_add_entities, + platform=PLATFORM_DOMAIN, + generator=VeraLock, ) diff --git a/homeassistant/components/vera/scene.py b/homeassistant/components/vera/scene.py index af5266ed4b3d7b..afb63e54bbc8b5 100644 --- a/homeassistant/components/vera/scene.py +++ b/homeassistant/components/vera/scene.py @@ -1,22 +1,31 @@ """Support for Vera scenes.""" import logging +from typing import Callable, List -from homeassistant.components.scene import Scene +from homeassistant.components.scene import DOMAIN as PLATFORM_DOMAIN, Scene +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -from . import VERA_CONTROLLER, VERA_ID_FORMAT, VERA_SCENES +from .common import setup_scene_entities +from .const import VERA_ID_FORMAT _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Vera scenes.""" - add_entities( - [ - VeraScene(scene, hass.data[VERA_CONTROLLER]) - for scene in hass.data[VERA_SCENES] - ], - True, +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up the sensor config entry.""" + setup_scene_entities( + hass=hass, + entry=entry, + async_add_entities=async_add_entities, + platform=PLATFORM_DOMAIN, + generator=VeraScene, ) diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py index 9ac0a36ff9cd00..1b395b473d3808 100644 --- a/homeassistant/components/vera/sensor.py +++ b/homeassistant/components/vera/sensor.py @@ -1,29 +1,36 @@ """Support for Vera sensors.""" from datetime import timedelta import logging +from typing import Callable, List import pyvera as veraApi -from homeassistant.components.sensor import ENTITY_ID_FORMAT -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_PERCENTAGE +from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN, ENTITY_ID_FORMAT +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.util import convert -from . import VERA_CONTROLLER, VERA_DEVICES, VeraDevice +from .common import VeraDevice, setup_device_entities _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=5) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Vera controller devices.""" - add_entities( - [ - VeraSensor(device, hass.data[VERA_CONTROLLER]) - for device in hass.data[VERA_DEVICES]["sensor"] - ], - True, +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up the sensor config entry.""" + setup_device_entities( + hass=hass, + entry=entry, + async_add_entities=async_add_entities, + platform=PLATFORM_DOMAIN, + generator=VeraSensor, ) diff --git a/homeassistant/components/vera/strings.json b/homeassistant/components/vera/strings.json new file mode 100644 index 00000000000000..decba97f1879ce --- /dev/null +++ b/homeassistant/components/vera/strings.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Vera" + } +} diff --git a/homeassistant/components/vera/switch.py b/homeassistant/components/vera/switch.py index ab3c3e6adb9555..f62ab42bdf5ff5 100644 --- a/homeassistant/components/vera/switch.py +++ b/homeassistant/components/vera/switch.py @@ -1,22 +1,34 @@ """Support for Vera switches.""" import logging +from typing import Callable, List -from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchDevice +from homeassistant.components.switch import ( + DOMAIN as PLATFORM_DOMAIN, + ENTITY_ID_FORMAT, + SwitchDevice, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity from homeassistant.util import convert -from . import VERA_CONTROLLER, VERA_DEVICES, VeraDevice +from .common import VeraDevice, setup_device_entities _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Vera switches.""" - add_entities( - [ - VeraSwitch(device, hass.data[VERA_CONTROLLER]) - for device in hass.data[VERA_DEVICES]["switch"] - ], - True, +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up the sensor config entry.""" + setup_device_entities( + hass=hass, + entry=entry, + async_add_entities=async_add_entities, + platform=PLATFORM_DOMAIN, + generator=VeraSwitch, ) diff --git a/tests/components/vera/common.py b/tests/components/vera/common.py index 649cf9af6a54e8..197d260b7a8fe9 100644 --- a/tests/components/vera/common.py +++ b/tests/components/vera/common.py @@ -1,47 +1,76 @@ """Common code for tests.""" -from typing import Callable, NamedTuple, Tuple +from typing import Callable, Dict, NamedTuple, Tuple from mock import MagicMock -from pyvera import VeraController, VeraDevice, VeraScene +import pyvera as pv -from homeassistant.components.vera import CONF_CONTROLLER, DOMAIN +from homeassistant.components.vera.const import CONF_CONTROLLER, DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component - -class ComponentData(NamedTuple): - """Component data.""" - - controller: VeraController +SetupCallback = Callable[[pv.VeraController], None] + +ControllerData = NamedTuple( + "ControllerData", (("controller", pv.VeraController), ("update_callback", Callable)) +) + +ComponentData = NamedTuple("ComponentData", (("controller_data", ControllerData),),) + +ControllerConfig = NamedTuple( + "ControllerConfig", + ( + ("config", Dict), + ("serial_number", str), + ("devices", Tuple[pv.VeraDevice, ...]), + ("scenes", Tuple[pv.VeraScene, ...]), + ("setup_callback", SetupCallback), + ), +) + + +def new_simple_controller_config( + base_url="http://127.0.0.1:123", + serial_number="1111", + devices: Tuple[pv.VeraDevice, ...] = (), + scenes: Tuple[pv.VeraScene, ...] = (), + setup_callback: SetupCallback = None, +) -> ControllerConfig: + """Create simple contorller config.""" + return ControllerConfig( + config={CONF_CONTROLLER: base_url}, + serial_number=serial_number, + devices=devices, + scenes=scenes, + setup_callback=setup_callback, + ) class ComponentFactory: """Factory class.""" - def __init__(self, init_controller_mock): - """Initialize component factory.""" - self.init_controller_mock = init_controller_mock + def __init__(self, vera_controller_class_mock): + """Constructor.""" + self.vera_controller_class_mock = vera_controller_class_mock async def configure_component( - self, - hass: HomeAssistant, - devices: Tuple[VeraDevice] = (), - scenes: Tuple[VeraScene] = (), - setup_callback: Callable[[VeraController], None] = None, + self, hass: HomeAssistant, controller_config: ControllerConfig ) -> ComponentData: """Configure the component with specific mock data.""" - controller_url = "http://127.0.0.1:123" - hass_config = { - DOMAIN: {CONF_CONTROLLER: controller_url}, + DOMAIN: controller_config.config, } - controller = MagicMock(spec=VeraController) # type: VeraController - controller.base_url = controller_url + controller = MagicMock(spec=pv.VeraController) # type: pv.VeraController + controller.base_url = controller_config.config.get(CONF_CONTROLLER) controller.register = MagicMock() - controller.get_devices = MagicMock(return_value=devices or ()) - controller.get_scenes = MagicMock(return_value=scenes or ()) + controller.start = MagicMock() + controller.stop = MagicMock() + controller.refresh_data = MagicMock() + controller.temperature_units = "C" + controller.serial_number = controller_config.serial_number + controller.get_devices = MagicMock(return_value=controller_config.devices) + controller.get_scenes = MagicMock(return_value=controller_config.scenes) for vera_obj in controller.get_devices() + controller.get_scenes(): vera_obj.vera_controller = controller @@ -49,17 +78,23 @@ async def configure_component( controller.get_devices.reset_mock() controller.get_scenes.reset_mock() - if setup_callback: - setup_callback(controller, hass_config) - - def init_controller(base_url: str) -> list: - nonlocal controller - return [controller, True] + if controller_config.setup_callback: + controller_config.setup_callback(controller, hass_config) - self.init_controller_mock.side_effect = init_controller + self.vera_controller_class_mock.return_value = controller # Setup Home Assistant. assert await async_setup_component(hass, DOMAIN, hass_config) await hass.async_block_till_done() - return ComponentData(controller=controller) + update_callback = ( + controller.register.call_args_list[0][0][1] + if controller.register.call_args_list + else None + ) + + return ComponentData( + controller_data=ControllerData( + controller=controller, update_callback=update_callback + ) + ) diff --git a/tests/components/vera/conftest.py b/tests/components/vera/conftest.py index b94a40135d8b09..2c15d3e4182fbe 100644 --- a/tests/components/vera/conftest.py +++ b/tests/components/vera/conftest.py @@ -9,5 +9,5 @@ @pytest.fixture() def vera_component_factory(): """Return a factory for initializing the vera component.""" - with patch("pyvera.init_controller") as init_controller_mock: - yield ComponentFactory(init_controller_mock) + with patch("pyvera.VeraController") as vera_controller_class_mock: + yield ComponentFactory(vera_controller_class_mock) diff --git a/tests/components/vera/test_binary_sensor.py b/tests/components/vera/test_binary_sensor.py index 2c2e2b8638818a..e4ea5197814110 100644 --- a/tests/components/vera/test_binary_sensor.py +++ b/tests/components/vera/test_binary_sensor.py @@ -5,7 +5,7 @@ from homeassistant.core import HomeAssistant -from .common import ComponentFactory +from .common import ComponentFactory, new_simple_controller_config async def test_binary_sensor( @@ -14,25 +14,23 @@ async def test_binary_sensor( """Test function.""" vera_device = MagicMock(spec=VeraBinarySensor) # type: VeraBinarySensor vera_device.device_id = 1 + vera_device.vera_device_id = 1 vera_device.name = "dev1" vera_device.is_tripped = False entity_id = "binary_sensor.dev1_1" component_data = await vera_component_factory.configure_component( - hass=hass, devices=(vera_device,) + hass=hass, + controller_config=new_simple_controller_config(devices=(vera_device,)), ) - controller = component_data.controller - - update_callback = controller.register.call_args_list[0][0][1] + update_callback = component_data.controller_data.update_callback vera_device.is_tripped = False update_callback(vera_device) await hass.async_block_till_done() assert hass.states.get(entity_id).state == "off" - controller.register.reset_mock() vera_device.is_tripped = True update_callback(vera_device) await hass.async_block_till_done() assert hass.states.get(entity_id).state == "on" - controller.register.reset_mock() diff --git a/tests/components/vera/test_climate.py b/tests/components/vera/test_climate.py index c27a72865fd995..bc8cd3562e1a75 100644 --- a/tests/components/vera/test_climate.py +++ b/tests/components/vera/test_climate.py @@ -13,7 +13,7 @@ ) from homeassistant.core import HomeAssistant -from .common import ComponentFactory +from .common import ComponentFactory, new_simple_controller_config async def test_climate( @@ -31,10 +31,10 @@ async def test_climate( entity_id = "climate.dev1_1" component_data = await vera_component_factory.configure_component( - hass=hass, devices=(vera_device,), + hass=hass, + controller_config=new_simple_controller_config(devices=(vera_device,)), ) - controller = component_data.controller - update_callback = controller.register.call_args_list[0][0][1] + update_callback = component_data.controller_data.update_callback assert hass.states.get(entity_id).state == HVAC_MODE_OFF @@ -137,10 +137,12 @@ def setup_callback(controller: VeraController, hass_config: dict) -> None: controller.temperature_units = "F" component_data = await vera_component_factory.configure_component( - hass=hass, devices=(vera_device,), setup_callback=setup_callback + hass=hass, + controller_config=new_simple_controller_config( + devices=(vera_device,), setup_callback=setup_callback + ), ) - controller = component_data.controller - update_callback = controller.register.call_args_list[0][0][1] + update_callback = component_data.controller_data.update_callback await hass.services.async_call( "climate", "set_temperature", {"entity_id": entity_id, "temperature": 30}, diff --git a/tests/components/vera/test_cover.py b/tests/components/vera/test_cover.py index 79cb4adedfbc92..3414419aa9f2c3 100644 --- a/tests/components/vera/test_cover.py +++ b/tests/components/vera/test_cover.py @@ -5,7 +5,7 @@ from homeassistant.core import HomeAssistant -from .common import ComponentFactory +from .common import ComponentFactory, new_simple_controller_config async def test_cover( @@ -21,10 +21,10 @@ async def test_cover( entity_id = "cover.dev1_1" component_data = await vera_component_factory.configure_component( - hass=hass, devices=(vera_device,), + hass=hass, + controller_config=new_simple_controller_config(devices=(vera_device,)), ) - controller = component_data.controller - update_callback = controller.register.call_args_list[0][0][1] + update_callback = component_data.controller_data.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 9ff6cb4058b3c0..baaf6ae56d73aa 100644 --- a/tests/components/vera/test_init.py +++ b/tests/components/vera/test_init.py @@ -1,78 +1,57 @@ """Vera tests.""" from unittest.mock import MagicMock -from pyvera import ( - VeraArmableDevice, - VeraBinarySensor, - VeraController, - VeraCurtain, - VeraDevice, - VeraDimmer, - VeraLock, - VeraScene, - VeraSceneController, - VeraSensor, - VeraSwitch, - VeraThermostat, -) +from pyvera import VeraBinarySensor -from homeassistant.components.vera import ( - CONF_EXCLUDE, - CONF_LIGHTS, - DOMAIN, - VERA_DEVICES, -) +from homeassistant.components.vera import DOMAIN, async_unload_entry from homeassistant.core import HomeAssistant -from .common import ComponentFactory - - -def new_vera_device(cls, device_id: int) -> VeraDevice: - """Create new mocked vera device..""" - vera_device = MagicMock(spec=cls) # type: VeraDevice - vera_device.device_id = device_id - vera_device.name = f"dev${device_id}" - return vera_device - - -def assert_hass_vera_devices(hass: HomeAssistant, platform: str, arr_len: int) -> None: - """Assert vera devices are present..""" - assert hass.data[VERA_DEVICES][platform] - assert len(hass.data[VERA_DEVICES][platform]) == arr_len +from .common import ComponentFactory, new_simple_controller_config async def test_init( hass: HomeAssistant, vera_component_factory: ComponentFactory ) -> None: """Test function.""" - - def setup_callback(controller: VeraController, hass_config: dict) -> None: - hass_config[DOMAIN][CONF_EXCLUDE] = [11] - hass_config[DOMAIN][CONF_LIGHTS] = [10] + vera_device1 = MagicMock(spec=VeraBinarySensor) # type: VeraBinarySensor + vera_device1.device_id = 1 + vera_device1.vera_device_id = 1 + vera_device1.name = "first_dev" + vera_device1.is_tripped = False + entity1_id = "binary_sensor.first_dev_1" await vera_component_factory.configure_component( hass=hass, - devices=( - new_vera_device(VeraDimmer, 1), - new_vera_device(VeraBinarySensor, 2), - new_vera_device(VeraSensor, 3), - new_vera_device(VeraArmableDevice, 4), - new_vera_device(VeraLock, 5), - new_vera_device(VeraThermostat, 6), - new_vera_device(VeraCurtain, 7), - new_vera_device(VeraSceneController, 8), - new_vera_device(VeraSwitch, 9), - new_vera_device(VeraSwitch, 10), - new_vera_device(VeraSwitch, 11), + controller_config=new_simple_controller_config( + base_url="http://127.0.0.1:111", + serial_number="first_serial", + devices=(vera_device1,), ), - scenes=(MagicMock(spec=VeraScene),), - setup_callback=setup_callback, ) - assert_hass_vera_devices(hass, "light", 2) - assert_hass_vera_devices(hass, "binary_sensor", 1) - assert_hass_vera_devices(hass, "sensor", 2) - assert_hass_vera_devices(hass, "switch", 2) - assert_hass_vera_devices(hass, "lock", 1) - assert_hass_vera_devices(hass, "climate", 1) - assert_hass_vera_devices(hass, "cover", 1) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + entry1 = entity_registry.async_get(entity1_id) + + assert entry1 + + +async def test_unload( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + vera_device1 = MagicMock(spec=VeraBinarySensor) # type: VeraBinarySensor + vera_device1.device_id = 1 + vera_device1.vera_device_id = 1 + vera_device1.name = "first_dev" + vera_device1.is_tripped = False + + component_data = await vera_component_factory.configure_component( + hass=hass, controller_config=new_simple_controller_config() + ) + component_data.controller_data.controller.stop() + + config_entries = hass.config_entries.async_entries(DOMAIN) + assert config_entries + + for config_entry in config_entries: + await async_unload_entry(hass, config_entry) diff --git a/tests/components/vera/test_light.py b/tests/components/vera/test_light.py index fa63ce63454444..0501a7dc789417 100644 --- a/tests/components/vera/test_light.py +++ b/tests/components/vera/test_light.py @@ -6,7 +6,7 @@ from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_HS_COLOR from homeassistant.core import HomeAssistant -from .common import ComponentFactory +from .common import ComponentFactory, new_simple_controller_config async def test_light( @@ -24,10 +24,10 @@ async def test_light( entity_id = "light.dev1_1" component_data = await vera_component_factory.configure_component( - hass=hass, devices=(vera_device,), + hass=hass, + controller_config=new_simple_controller_config(devices=(vera_device,)), ) - controller = component_data.controller - update_callback = controller.register.call_args_list[0][0][1] + update_callback = component_data.controller_data.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 362bdbeddc0fde..95be83cc4d042d 100644 --- a/tests/components/vera/test_lock.py +++ b/tests/components/vera/test_lock.py @@ -6,7 +6,7 @@ from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED from homeassistant.core import HomeAssistant -from .common import ComponentFactory +from .common import ComponentFactory, new_simple_controller_config async def test_lock( @@ -21,10 +21,10 @@ async def test_lock( entity_id = "lock.dev1_1" component_data = await vera_component_factory.configure_component( - hass=hass, devices=(vera_device,), + hass=hass, + controller_config=new_simple_controller_config(devices=(vera_device,)), ) - controller = component_data.controller - update_callback = controller.register.call_args_list[0][0][1] + update_callback = component_data.controller_data.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 136227ffa7126e..d32f639cc24e53 100644 --- a/tests/components/vera/test_scene.py +++ b/tests/components/vera/test_scene.py @@ -5,7 +5,7 @@ from homeassistant.core import HomeAssistant -from .common import ComponentFactory +from .common import ComponentFactory, new_simple_controller_config async def test_scene( @@ -18,7 +18,7 @@ async def test_scene( entity_id = "scene.dev1_1" await vera_component_factory.configure_component( - hass=hass, scenes=(vera_scene,), + hass=hass, controller_config=new_simple_controller_config(scenes=(vera_scene,)), ) await hass.services.async_call( diff --git a/tests/components/vera/test_sensor.py b/tests/components/vera/test_sensor.py index 9e84815d636230..bdae3313f4a37f 100644 --- a/tests/components/vera/test_sensor.py +++ b/tests/components/vera/test_sensor.py @@ -16,7 +16,7 @@ from homeassistant.const import UNIT_PERCENTAGE from homeassistant.core import HomeAssistant -from .common import ComponentFactory +from .common import ComponentFactory, new_simple_controller_config async def run_sensor_test( @@ -37,10 +37,12 @@ async def run_sensor_test( entity_id = "sensor.dev1_1" component_data = await vera_component_factory.configure_component( - hass=hass, devices=(vera_device,), setup_callback=setup_callback + hass=hass, + controller_config=new_simple_controller_config( + devices=(vera_device,), setup_callback=setup_callback + ), ) - controller = component_data.controller - update_callback = controller.register.call_args_list[0][0][1] + update_callback = component_data.controller_data.update_callback for (initial_value, state_value) in assert_states: setattr(vera_device, class_property, initial_value) @@ -188,10 +190,10 @@ async def test_scene_controller_sensor( entity_id = "sensor.dev1_1" component_data = await vera_component_factory.configure_component( - hass=hass, devices=(vera_device,) + hass=hass, + controller_config=new_simple_controller_config(devices=(vera_device,)), ) - controller = component_data.controller - update_callback = controller.register.call_args_list[0][0][1] + update_callback = component_data.controller_data.update_callback vera_device.get_last_scene_time = "1111" update_callback(vera_device) diff --git a/tests/components/vera/test_switch.py b/tests/components/vera/test_switch.py index ba09068e7e602f..a80928366a1540 100644 --- a/tests/components/vera/test_switch.py +++ b/tests/components/vera/test_switch.py @@ -5,7 +5,7 @@ from homeassistant.core import HomeAssistant -from .common import ComponentFactory +from .common import ComponentFactory, new_simple_controller_config async def test_switch( @@ -20,10 +20,10 @@ async def test_switch( entity_id = "switch.dev1_1" component_data = await vera_component_factory.configure_component( - hass=hass, devices=(vera_device,), + hass=hass, + controller_config=new_simple_controller_config(devices=(vera_device,)), ) - controller = component_data.controller - update_callback = controller.register.call_args_list[0][0][1] + update_callback = component_data.controller_data.update_callback assert hass.states.get(entity_id).state == "off" From 94382a7ccb64156b9eb3b6bc441bb23f221fdbd3 Mon Sep 17 00:00:00 2001 From: Robbie Van Gorkom Date: Thu, 12 Dec 2019 08:37:58 -0800 Subject: [PATCH 02/21] Fixing lint error. --- tests/components/vera/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/vera/common.py b/tests/components/vera/common.py index 197d260b7a8fe9..89ce631f167a73 100644 --- a/tests/components/vera/common.py +++ b/tests/components/vera/common.py @@ -50,7 +50,7 @@ class ComponentFactory: """Factory class.""" def __init__(self, vera_controller_class_mock): - """Constructor.""" + """Initialize the factory.""" self.vera_controller_class_mock = vera_controller_class_mock async def configure_component( From 07ec5bf7cdff283f2e7f7ecc76dda9ea00f1a483 Mon Sep 17 00:00:00 2001 From: Robbie Van Gorkom Date: Thu, 12 Dec 2019 19:47:53 -0800 Subject: [PATCH 03/21] Applying minimal changes necessary to get config entries working. --- homeassistant/components/vera/__init__.py | 163 ++++++++++++++++- .../components/vera/binary_sensor.py | 3 +- homeassistant/components/vera/climate.py | 3 +- homeassistant/components/vera/common.py | 170 +----------------- homeassistant/components/vera/cover.py | 3 +- homeassistant/components/vera/light.py | 3 +- homeassistant/components/vera/lock.py | 3 +- homeassistant/components/vera/sensor.py | 3 +- homeassistant/components/vera/switch.py | 3 +- tests/components/vera/common.py | 6 +- tests/components/vera/conftest.py | 4 +- 11 files changed, 179 insertions(+), 185 deletions(-) diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index eb44d827382c23..e91a6e443fa874 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -1,27 +1,44 @@ """Support for Vera devices.""" +from collections import defaultdict import logging +import pyvera as veraApi from requests.exceptions import RequestException import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS +from homeassistant.const import ( + ATTR_ARMED, + ATTR_BATTERY_LEVEL, + ATTR_LAST_TRIP_TIME, + ATTR_TRIPPED, + CONF_EXCLUDE, + CONF_LIGHTS, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.core import HomeAssistant 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, get_configured_platforms, get_controller_data_by_config, - initialize_controller, set_controller_data, ) -from .const import CONF_CONTROLLER, DOMAIN +from .const import ( + ATTR_CURRENT_ENERGY_KWH, + ATTR_CURRENT_POWER_W, + CONF_CONTROLLER, + DOMAIN, + VERA_ID_FORMAT, +) _LOGGER = logging.getLogger(__name__) -VERA_CONTROLLER = "vera_controller" - VERA_ID_LIST_SCHEMA = vol.Schema([int]) CONFIG_SCHEMA = vol.Schema( @@ -71,13 +88,48 @@ def setup(hass: HomeAssistant, base_config: dict) -> bool: async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Do setup of vera.""" + + config = config_entry.data + + # Get Vera specific configuration. + base_url = config.get(CONF_CONTROLLER) + light_ids = config.get(CONF_LIGHTS) + exclude_ids = config.get(CONF_EXCLUDE) + + # Initialize the Vera controller. + controller, _ = veraApi.init_controller(base_url) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, lambda event: controller.stop() + ) + try: - controller_data = initialize_controller(hass, config_entry.data) + all_devices = controller.get_devices() + + all_scenes = controller.get_scenes() except RequestException: # There was a network related error connecting to the Vera controller. _LOGGER.exception("Error communicating with Vera API") return False + # Exclude devices unwanted by user. + devices = [device for device in all_devices if device.device_id not in exclude_ids] + + 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) + + vera_scenes = [] + for scene in all_scenes: + vera_scenes.append(scene) + + controller_data = ControllerData( + controller=controller, devices=vera_devices, scenes=vera_scenes + ) + set_controller_data(hass, controller_data) # Forward the config data to the necessary platforms. @@ -102,3 +154,102 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> ) return True + + +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: + return "light" + return "switch" + return None + + +class VeraDevice(Entity): + """Representation of a Vera device entity.""" + + def __init__(self, vera_device, controller): + """Initialize the device.""" + self.vera_device = vera_device + self.controller = 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 + ) + + self.controller.register(vera_device, self._update_callback) + + def _update_callback(self, _device): + """Update the state.""" + self.schedule_update_ha_state(True) + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def should_poll(self): + """Get polling requirement from vera device.""" + return self.vera_device.should_poll + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + attr = {} + + if self.vera_device.has_battery: + attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + + if self.vera_device.is_armable: + armed = self.vera_device.is_armed + attr[ATTR_ARMED] = "True" if armed else "False" + + if self.vera_device.is_trippable: + last_tripped = self.vera_device.last_trip + if last_tripped is not None: + utc_time = utc_from_timestamp(int(last_tripped)) + attr[ATTR_LAST_TRIP_TIME] = utc_time.isoformat() + else: + attr[ATTR_LAST_TRIP_TIME] = None + tripped = self.vera_device.is_tripped + attr[ATTR_TRIPPED] = "True" if tripped else "False" + + power = self.vera_device.power + if power: + attr[ATTR_CURRENT_POWER_W] = convert(power, float, 0.0) + + energy = self.vera_device.energy + if energy: + attr[ATTR_CURRENT_ENERGY_KWH] = convert(energy, float, 0.0) + + attr["Vera Device Id"] = self.vera_device.vera_device_id + + return attr + + @property + def unique_id(self) -> str: + """Return a unique ID. + + The Vera assigns a unique and immutable ID number to each device. + """ + return str(self.vera_device.vera_device_id) diff --git a/homeassistant/components/vera/binary_sensor.py b/homeassistant/components/vera/binary_sensor.py index ec693c3ada67ec..8d487538899bfe 100644 --- a/homeassistant/components/vera/binary_sensor.py +++ b/homeassistant/components/vera/binary_sensor.py @@ -11,7 +11,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from .common import VeraDevice, setup_device_entities +from . import VeraDevice +from .common import setup_device_entities _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index e3dd9a64af71cd..3e8c8078b533f8 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -23,7 +23,8 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import convert -from .common import VeraDevice, setup_device_entities +from . import VeraDevice +from .common import setup_device_entities _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vera/common.py b/homeassistant/components/vera/common.py index 9e5e8713665c36..c2f1d09250a7fb 100644 --- a/homeassistant/components/vera/common.py +++ b/homeassistant/components/vera/common.py @@ -1,41 +1,15 @@ """Common vera code.""" -from collections import defaultdict import logging -from typing import Callable, DefaultDict, List, NamedTuple, Optional, Union +from typing import Callable, DefaultDict, List, NamedTuple, Optional, Set, Union import pyvera as pv -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN -from homeassistant.components.cover import DOMAIN as COVER_DOMAIN -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, Scene -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ARMED, - ATTR_BATTERY_LEVEL, - ATTR_LAST_TRIP_TIME, - ATTR_TRIPPED, - CONF_EXCLUDE, - CONF_LIGHTS, - EVENT_HOMEASSISTANT_STOP, -) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.util import convert, slugify -from homeassistant.util.dt import utc_from_timestamp -from .const import ( - ATTR_CURRENT_ENERGY_KWH, - ATTR_CURRENT_POWER_W, - CONF_CONTROLLER, - CONTROLLER_DATAS, - DOMAIN, - VERA_ID_FORMAT, -) +from .const import CONF_CONTROLLER, CONTROLLER_DATAS, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -49,118 +23,6 @@ ), ) - -class VeraDevice(Entity): - """Representation of a Vera device entity.""" - - def __init__(self, vera_device: pv.VeraDevice, controller: pv.VeraController): - """Initialize the device.""" - self.vera_device = vera_device - self.controller = 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 - ) - - self.controller.register(vera_device, self._update_callback) - - def _update_callback(self, _device): - """Update the state.""" - self.schedule_update_ha_state(True) - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def should_poll(self): - """Get polling requirement from vera device.""" - return self.vera_device.should_poll - - @property - def device_state_attributes(self): - """Return the state attributes of the device.""" - attr = {} - - if self.vera_device.has_battery: - attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level - - if self.vera_device.is_armable: - armed = self.vera_device.is_armed - attr[ATTR_ARMED] = "True" if armed else "False" - - if self.vera_device.is_trippable: - last_tripped = self.vera_device.last_trip - if last_tripped is not None: - utc_time = utc_from_timestamp(int(last_tripped)) - attr[ATTR_LAST_TRIP_TIME] = utc_time.isoformat() - else: - attr[ATTR_LAST_TRIP_TIME] = None - tripped = self.vera_device.is_tripped - attr[ATTR_TRIPPED] = "True" if tripped else "False" - - power = self.vera_device.power - if power: - attr[ATTR_CURRENT_POWER_W] = convert(power, float, 0.0) - - energy = self.vera_device.energy - if energy: - attr[ATTR_CURRENT_ENERGY_KWH] = convert(energy, float, 0.0) - - attr["Vera Device Id"] = self.vera_device.vera_device_id - - return attr - - @property - def unique_id(self) -> str: - """Return a unique ID. - - The Vera assigns a unique and immutable ID number to each device. - """ - return str(self.vera_device.vera_device_id) - - -def initialize_controller(hass: HomeAssistant, config: dict) -> ControllerData: - """Initialize a controller.""" - # Get Vera specific configuration. - base_url = config.get(CONF_CONTROLLER) - light_ids = config.get(CONF_LIGHTS) - exclude_ids = config.get(CONF_EXCLUDE) - - # Initialize the Vera controller. - controller = pv.VeraController(base_url) - controller.start() - - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, lambda event: controller.stop() - ) - - controller.refresh_data() - all_devices = controller.get_devices() - all_scenes = controller.get_scenes() - # Exclude devices unwanted by user. - devices = [device for device in all_devices if device.device_id not in exclude_ids] - - 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) - - vera_scenes = [] - for scene in all_scenes: - vera_scenes.append(scene) - - return ControllerData( - controller=controller, devices=vera_devices, scenes=vera_scenes - ) - - EntityDeviceGenerator = Callable[[pv.VeraDevice, pv.VeraController], Entity] EntitySceneGenerator = Callable[[pv.VeraDevice, pv.VeraController], Scene] EntityGenerator = Union[EntityDeviceGenerator, EntitySceneGenerator] @@ -228,32 +90,6 @@ def _setup_entities( async_add_entities(entities, True) -def map_vera_device(vera_device, remap): - """Map vera classes to Home Assistant types.""" - - if isinstance(vera_device, pv.VeraDimmer): - return LIGHT_DOMAIN - if isinstance(vera_device, pv.VeraBinarySensor): - return BINARY_SENSOR_DOMAIN - if isinstance(vera_device, pv.VeraSensor): - return SENSOR_DOMAIN - if isinstance(vera_device, pv.VeraArmableDevice): - return SWITCH_DOMAIN - if isinstance(vera_device, pv.VeraLock): - return LOCK_DOMAIN - if isinstance(vera_device, pv.VeraThermostat): - return CLIMATE_DOMAIN - if isinstance(vera_device, pv.VeraCurtain): - return COVER_DOMAIN - if isinstance(vera_device, pv.VeraSceneController): - return SENSOR_DOMAIN - if isinstance(vera_device, pv.VeraSwitch): - if vera_device.device_id in remap: - return LIGHT_DOMAIN - return SWITCH_DOMAIN - return None - - def get_controller_data_by_config( hass: HomeAssistant, entry: ConfigEntry ) -> Optional[ControllerData]: @@ -274,7 +110,7 @@ def set_controller_data(hass: HomeAssistant, controller_data: ControllerData) -> hass.data[DOMAIN][CONTROLLER_DATAS][serial_number] = controller_data -def get_configured_platforms(controller_data: ControllerData) -> List[str]: +def get_configured_platforms(controller_data: ControllerData) -> Set[str]: """Get configured platforms for a controller.""" platforms = [] for platform in controller_data.devices.keys(): diff --git a/homeassistant/components/vera/cover.py b/homeassistant/components/vera/cover.py index b11d680af369e0..bad1a4e3fae88e 100644 --- a/homeassistant/components/vera/cover.py +++ b/homeassistant/components/vera/cover.py @@ -12,7 +12,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from .common import VeraDevice, setup_device_entities +from . import VeraDevice +from .common import setup_device_entities _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vera/light.py b/homeassistant/components/vera/light.py index 696ac163f56829..d54679d0555a6f 100644 --- a/homeassistant/components/vera/light.py +++ b/homeassistant/components/vera/light.py @@ -16,7 +16,8 @@ from homeassistant.helpers.entity import Entity import homeassistant.util.color as color_util -from .common import VeraDevice, setup_device_entities +from . import VeraDevice +from .common import setup_device_entities _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vera/lock.py b/homeassistant/components/vera/lock.py index e0dacbdedd3caf..9697c7ab86320b 100644 --- a/homeassistant/components/vera/lock.py +++ b/homeassistant/components/vera/lock.py @@ -12,7 +12,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from .common import VeraDevice, setup_device_entities +from . import VeraDevice +from .common import setup_device_entities _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py index 1b395b473d3808..29137011634084 100644 --- a/homeassistant/components/vera/sensor.py +++ b/homeassistant/components/vera/sensor.py @@ -12,7 +12,8 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import convert -from .common import VeraDevice, setup_device_entities +from . import VeraDevice +from .common import setup_device_entities _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vera/switch.py b/homeassistant/components/vera/switch.py index f62ab42bdf5ff5..70831fddbe2d34 100644 --- a/homeassistant/components/vera/switch.py +++ b/homeassistant/components/vera/switch.py @@ -12,7 +12,8 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import convert -from .common import VeraDevice, setup_device_entities +from . import VeraDevice +from .common import setup_device_entities _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/vera/common.py b/tests/components/vera/common.py index 89ce631f167a73..28919aff3c5c04 100644 --- a/tests/components/vera/common.py +++ b/tests/components/vera/common.py @@ -49,9 +49,9 @@ def new_simple_controller_config( class ComponentFactory: """Factory class.""" - def __init__(self, vera_controller_class_mock): + def __init__(self, init_controller_mock): """Initialize the factory.""" - self.vera_controller_class_mock = vera_controller_class_mock + self.init_controller_mock = init_controller_mock async def configure_component( self, hass: HomeAssistant, controller_config: ControllerConfig @@ -81,7 +81,7 @@ async def configure_component( if controller_config.setup_callback: controller_config.setup_callback(controller, hass_config) - self.vera_controller_class_mock.return_value = controller + self.init_controller_mock.return_value = [controller, 0] # Setup Home Assistant. assert await async_setup_component(hass, DOMAIN, hass_config) diff --git a/tests/components/vera/conftest.py b/tests/components/vera/conftest.py index 2c15d3e4182fbe..b94a40135d8b09 100644 --- a/tests/components/vera/conftest.py +++ b/tests/components/vera/conftest.py @@ -9,5 +9,5 @@ @pytest.fixture() def vera_component_factory(): """Return a factory for initializing the vera component.""" - with patch("pyvera.VeraController") as vera_controller_class_mock: - yield ComponentFactory(vera_controller_class_mock) + with patch("pyvera.init_controller") as init_controller_mock: + yield ComponentFactory(init_controller_mock) From 6460f21ef63cbaed67ffa81db0ea9f3ea0094088 Mon Sep 17 00:00:00 2001 From: Robbie Van Gorkom Date: Sat, 14 Dec 2019 08:19:06 -0800 Subject: [PATCH 04/21] Addressing PR feedback by further reducing the scope of the change. --- homeassistant/components/vera/__init__.py | 11 +-- .../components/vera/binary_sensor.py | 14 +-- homeassistant/components/vera/climate.py | 14 +-- homeassistant/components/vera/common.py | 95 +------------------ homeassistant/components/vera/cover.py | 14 +-- homeassistant/components/vera/light.py | 14 +-- homeassistant/components/vera/lock.py | 14 +-- homeassistant/components/vera/scene.py | 17 ++-- homeassistant/components/vera/sensor.py | 14 +-- homeassistant/components/vera/switch.py | 14 +-- tests/components/vera/test_sensor.py | 2 +- 11 files changed, 63 insertions(+), 160 deletions(-) diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index e91a6e443fa874..f5210caa508da0 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -23,12 +23,7 @@ from homeassistant.util import convert, slugify from homeassistant.util.dt import utc_from_timestamp -from .common import ( - ControllerData, - get_configured_platforms, - get_controller_data_by_config, - set_controller_data, -) +from .common import ControllerData, get_configured_platforms from .const import ( ATTR_CURRENT_ENERGY_KWH, ATTR_CURRENT_POWER_W, @@ -130,7 +125,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): controller=controller, devices=vera_devices, scenes=vera_scenes ) - set_controller_data(hass, controller_data) + hass.data[DOMAIN] = controller_data # Forward the config data to the necessary platforms. for platform in get_configured_platforms(controller_data): @@ -143,7 +138,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload Withings config entry.""" - controller_data = get_controller_data_by_config(hass=hass, entry=config_entry) + controller_data = hass.data[DOMAIN] if not controller_data: return True diff --git a/homeassistant/components/vera/binary_sensor.py b/homeassistant/components/vera/binary_sensor.py index 8d487538899bfe..621dc09930d99f 100644 --- a/homeassistant/components/vera/binary_sensor.py +++ b/homeassistant/components/vera/binary_sensor.py @@ -12,7 +12,7 @@ from homeassistant.helpers.entity import Entity from . import VeraDevice -from .common import setup_device_entities +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -23,12 +23,12 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" - setup_device_entities( - hass=hass, - entry=entry, - async_add_entities=async_add_entities, - platform=PLATFORM_DOMAIN, - generator=VeraBinarySensor, + controller_data = hass.data[DOMAIN] + async_add_entities( + [ + VeraBinarySensor(device, controller_data.controller) + for device in controller_data.devices.get(PLATFORM_DOMAIN) + ] ) diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index 3e8c8078b533f8..520c3b516df34f 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -24,7 +24,7 @@ from homeassistant.util import convert from . import VeraDevice -from .common import setup_device_entities +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -40,12 +40,12 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" - setup_device_entities( - hass=hass, - entry=entry, - async_add_entities=async_add_entities, - platform=PLATFORM_DOMAIN, - generator=VeraThermostat, + controller_data = hass.data[DOMAIN] + async_add_entities( + [ + VeraThermostat(device, controller_data.controller) + for device in controller_data.devices.get(PLATFORM_DOMAIN) + ] ) diff --git a/homeassistant/components/vera/common.py b/homeassistant/components/vera/common.py index c2f1d09250a7fb..9cb02c33ed2a16 100644 --- a/homeassistant/components/vera/common.py +++ b/homeassistant/components/vera/common.py @@ -1,15 +1,10 @@ """Common vera code.""" import logging -from typing import Callable, DefaultDict, List, NamedTuple, Optional, Set, Union +from typing import DefaultDict, List, NamedTuple, Set import pyvera as pv -from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, Scene -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity - -from .const import CONF_CONTROLLER, CONTROLLER_DATAS, DOMAIN +from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN _LOGGER = logging.getLogger(__name__) @@ -23,92 +18,6 @@ ), ) -EntityDeviceGenerator = Callable[[pv.VeraDevice, pv.VeraController], Entity] -EntitySceneGenerator = Callable[[pv.VeraDevice, pv.VeraController], Scene] -EntityGenerator = Union[EntityDeviceGenerator, EntitySceneGenerator] -ItemCollector = Callable[ - [ControllerData, str], List[Union[pv.VeraDevice, pv.VeraScene]] -] - - -def setup_device_entities( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], - platform: str, - generator: EntityDeviceGenerator, -): - """Create and add vera entities for devices in a platform.""" - - _setup_entities( - hass=hass, - entry=entry, - async_add_entities=async_add_entities, - platform=platform, - generator=generator, - item_collector=lambda controller_data, platform: controller_data.devices.get( - platform - ), - ) - - -def setup_scene_entities( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], - platform: str, - generator: EntitySceneGenerator, -): - """Create and add vera scenes.""" - _setup_entities( - hass=hass, - entry=entry, - async_add_entities=async_add_entities, - platform=platform, - generator=generator, - item_collector=lambda controller_data, platform: controller_data.scenes, - ) - - -def _setup_entities( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], - platform: str, - generator: EntityGenerator, - item_collector: ItemCollector, -) -> None: - """Create and add vera entities for a given platform.""" - controller_data = get_controller_data_by_config(hass=hass, entry=entry) - - entities = [] - items = item_collector(controller_data, platform) - - for item in items or []: - entities.append(generator(item, controller_data.controller)) - - async_add_entities(entities, True) - - -def get_controller_data_by_config( - hass: HomeAssistant, entry: ConfigEntry -) -> Optional[ControllerData]: - """Get controller data from hass data.""" - base_url = entry.data.get(CONF_CONTROLLER) - for controller_data in hass.data[DOMAIN][CONTROLLER_DATAS].values(): - if controller_data.controller.base_url == base_url: - return controller_data - - return None - - -def set_controller_data(hass: HomeAssistant, controller_data: ControllerData) -> None: - """Set controller data in hass data.""" - serial_number = controller_data.controller.serial_number - hass.data[DOMAIN] = hass.data.get(DOMAIN, {}) - hass.data[DOMAIN][CONTROLLER_DATAS] = hass.data[DOMAIN].get(CONTROLLER_DATAS, {}) - hass.data[DOMAIN][CONTROLLER_DATAS][serial_number] = controller_data - def get_configured_platforms(controller_data: ControllerData) -> Set[str]: """Get configured platforms for a controller.""" diff --git a/homeassistant/components/vera/cover.py b/homeassistant/components/vera/cover.py index bad1a4e3fae88e..0d0edb841c1ec0 100644 --- a/homeassistant/components/vera/cover.py +++ b/homeassistant/components/vera/cover.py @@ -13,7 +13,7 @@ from homeassistant.helpers.entity import Entity from . import VeraDevice -from .common import setup_device_entities +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -24,12 +24,12 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" - setup_device_entities( - hass=hass, - entry=entry, - async_add_entities=async_add_entities, - platform=PLATFORM_DOMAIN, - generator=VeraCover, + controller_data = hass.data[DOMAIN] + async_add_entities( + [ + VeraCover(device, controller_data.controller) + for device in controller_data.devices.get(PLATFORM_DOMAIN) + ] ) diff --git a/homeassistant/components/vera/light.py b/homeassistant/components/vera/light.py index d54679d0555a6f..877fdf51f0a48d 100644 --- a/homeassistant/components/vera/light.py +++ b/homeassistant/components/vera/light.py @@ -17,7 +17,7 @@ import homeassistant.util.color as color_util from . import VeraDevice -from .common import setup_device_entities +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -28,12 +28,12 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" - setup_device_entities( - hass=hass, - entry=entry, - async_add_entities=async_add_entities, - platform=PLATFORM_DOMAIN, - generator=VeraLight, + controller_data = hass.data[DOMAIN] + async_add_entities( + [ + VeraLight(device, controller_data.controller) + for device in controller_data.devices.get(PLATFORM_DOMAIN) + ] ) diff --git a/homeassistant/components/vera/lock.py b/homeassistant/components/vera/lock.py index 9697c7ab86320b..da3c432a6afa91 100644 --- a/homeassistant/components/vera/lock.py +++ b/homeassistant/components/vera/lock.py @@ -13,7 +13,7 @@ from homeassistant.helpers.entity import Entity from . import VeraDevice -from .common import setup_device_entities +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -27,12 +27,12 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" - setup_device_entities( - hass=hass, - entry=entry, - async_add_entities=async_add_entities, - platform=PLATFORM_DOMAIN, - generator=VeraLock, + controller_data = hass.data[DOMAIN] + async_add_entities( + [ + VeraLock(device, controller_data.controller) + for device in controller_data.devices.get(PLATFORM_DOMAIN) + ] ) diff --git a/homeassistant/components/vera/scene.py b/homeassistant/components/vera/scene.py index afb63e54bbc8b5..7d09e248893881 100644 --- a/homeassistant/components/vera/scene.py +++ b/homeassistant/components/vera/scene.py @@ -2,14 +2,13 @@ import logging from typing import Callable, List -from homeassistant.components.scene import DOMAIN as PLATFORM_DOMAIN, Scene +from homeassistant.components.scene import Scene from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -from .common import setup_scene_entities -from .const import VERA_ID_FORMAT +from .const import DOMAIN, VERA_ID_FORMAT _LOGGER = logging.getLogger(__name__) @@ -20,12 +19,12 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" - setup_scene_entities( - hass=hass, - entry=entry, - async_add_entities=async_add_entities, - platform=PLATFORM_DOMAIN, - generator=VeraScene, + controller_data = hass.data[DOMAIN] + async_add_entities( + [ + VeraScene(device, controller_data.controller) + for device in controller_data.scenes + ] ) diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py index 29137011634084..82188b49a46b44 100644 --- a/homeassistant/components/vera/sensor.py +++ b/homeassistant/components/vera/sensor.py @@ -13,7 +13,7 @@ from homeassistant.util import convert from . import VeraDevice -from .common import setup_device_entities +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -26,12 +26,12 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" - setup_device_entities( - hass=hass, - entry=entry, - async_add_entities=async_add_entities, - platform=PLATFORM_DOMAIN, - generator=VeraSensor, + controller_data = hass.data[DOMAIN] + async_add_entities( + [ + VeraSensor(device, controller_data.controller) + for device in controller_data.devices.get(PLATFORM_DOMAIN) + ] ) diff --git a/homeassistant/components/vera/switch.py b/homeassistant/components/vera/switch.py index 70831fddbe2d34..a7ae6d45573e41 100644 --- a/homeassistant/components/vera/switch.py +++ b/homeassistant/components/vera/switch.py @@ -13,7 +13,7 @@ from homeassistant.util import convert from . import VeraDevice -from .common import setup_device_entities +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -24,12 +24,12 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" - setup_device_entities( - hass=hass, - entry=entry, - async_add_entities=async_add_entities, - platform=PLATFORM_DOMAIN, - generator=VeraSwitch, + controller_data = hass.data[DOMAIN] + async_add_entities( + [ + VeraSwitch(device, controller_data.controller) + for device in controller_data.devices.get(PLATFORM_DOMAIN) + ] ) diff --git a/tests/components/vera/test_sensor.py b/tests/components/vera/test_sensor.py index bdae3313f4a37f..d8b2a27b0de2b7 100644 --- a/tests/components/vera/test_sensor.py +++ b/tests/components/vera/test_sensor.py @@ -195,7 +195,7 @@ async def test_scene_controller_sensor( ) update_callback = component_data.controller_data.update_callback - vera_device.get_last_scene_time = "1111" + vera_device.get_last_scene_time.return_value = "1111" update_callback(vera_device) await hass.async_block_till_done() assert hass.states.get(entity_id).state == "id0" From db0840abc5905da1e8a6df35522c2b963e8382e8 Mon Sep 17 00:00:00 2001 From: Robbie Van Gorkom Date: Sun, 15 Dec 2019 07:37:29 -0800 Subject: [PATCH 05/21] Addressing PR feedback. --- homeassistant/components/vera/__init__.py | 29 ++++++++++--------- homeassistant/components/vera/common.py | 2 +- homeassistant/components/vera/config_flow.py | 18 ++++++++++-- homeassistant/components/vera/strings.json | 5 +++- tests/components/vera/test_config_flow.py | 30 ++++++++++++++++++++ tests/components/vera/test_init.py | 29 ++++++++++++++++--- 6 files changed, 90 insertions(+), 23 deletions(-) create mode 100644 tests/components/vera/test_config_flow.py diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index f5210caa508da0..efd6160ae6235a 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -1,4 +1,5 @@ """Support for Vera devices.""" +import asyncio from collections import defaultdict import logging @@ -50,12 +51,16 @@ ) -def setup(hass: HomeAssistant, base_config: dict) -> bool: +async def async_setup(hass: HomeAssistant, base_config: dict) -> bool: """Set up for Vera controllers.""" - config = base_config.get(DOMAIN, []) + config = base_config.get(DOMAIN) # Normalize the base url. config[CONF_CONTROLLER] = config.get(CONF_CONTROLLER).rstrip("/") + base_url = config.get(CONF_CONTROLLER) + + controller, _ = veraApi.init_controller(base_url) + hass.data[DOMAIN] = ControllerData(controller=controller, devices=(), scenes=()) # Build a map of already configured controllers. base_url_entries_map = {} @@ -63,8 +68,6 @@ def setup(hass: HomeAssistant, base_config: dict) -> bool: base_url = config_entry.data.get(CONF_CONTROLLER) base_url_entries_map[base_url] = config_entry - base_url = config.get(CONF_CONTROLLER) - entry = base_url_entries_map.get(base_url) if entry: _LOGGER.debug("Updating existing config for %s", base_url) @@ -87,14 +90,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): config = config_entry.data # Get Vera specific configuration. - base_url = config.get(CONF_CONTROLLER) light_ids = config.get(CONF_LIGHTS) exclude_ids = config.get(CONF_EXCLUDE) # Initialize the Vera controller. - controller, _ = veraApi.init_controller(base_url) + controller = hass.data[DOMAIN].controller # type: veraApi.VeraController hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, lambda event: controller.stop() + EVENT_HOMEASSISTANT_STOP, + lambda event: hass.async_add_executor_job(controller.stop), ) try: @@ -140,13 +143,11 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> """Unload Withings config entry.""" controller_data = hass.data[DOMAIN] - if not controller_data: - return True - - for platform in get_configured_platforms(controller_data): - hass.async_create_task( - hass.config_entries.async_forward_entry_unload(config_entry, platform) - ) + tasks = [ + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in get_configured_platforms(controller_data) + ] + await asyncio.gather(*tasks) return True diff --git a/homeassistant/components/vera/common.py b/homeassistant/components/vera/common.py index 9cb02c33ed2a16..d9f3bc493226f6 100644 --- a/homeassistant/components/vera/common.py +++ b/homeassistant/components/vera/common.py @@ -22,7 +22,7 @@ def get_configured_platforms(controller_data: ControllerData) -> Set[str]: """Get configured platforms for a controller.""" platforms = [] - for platform in controller_data.devices.keys(): + for platform in controller_data.devices: platforms.append(platform) if controller_data.scenes: diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py index 1065e8f8763d2d..c9ffb20a3427fe 100644 --- a/homeassistant/components/vera/config_flow.py +++ b/homeassistant/components/vera/config_flow.py @@ -1,13 +1,25 @@ """Config flow for Vera.""" +from requests.exceptions import RequestException + from homeassistant import config_entries from .const import CONF_CONTROLLER, DOMAIN -@config_entries.HANDLERS.register(DOMAIN) -class VeraFlowHandler(config_entries.ConfigFlow): +class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Vera config flow.""" async def async_step_import(self, config: dict): """Handle a flow initialized by import.""" - return self.async_create_entry(title=config.get(CONF_CONTROLLER), data=config) + base_url = config.get(CONF_CONTROLLER) + + controller = self.hass.data[DOMAIN].controller + + try: + controller.refresh_data() + except RequestException: + return self.async_abort( + reason="cannot-connect", description_placeholders={"base_url": base_url} + ) + + return self.async_create_entry(title=base_url, data=config) diff --git a/homeassistant/components/vera/strings.json b/homeassistant/components/vera/strings.json index decba97f1879ce..97ac9d0415ae9f 100644 --- a/homeassistant/components/vera/strings.json +++ b/homeassistant/components/vera/strings.json @@ -1,5 +1,8 @@ { "config": { - "title": "Vera" + "title": "Vera", + "abort": { + "cannot-connect": "Could not connect to controller with url {base_url}" + } } } diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py new file mode 100644 index 00000000000000..cb1cacb2afc42b --- /dev/null +++ b/tests/components/vera/test_config_flow.py @@ -0,0 +1,30 @@ +"""Vera tests.""" +from unittest.mock import MagicMock + +from pyvera import VeraController +from requests.exceptions import RequestException + +from homeassistant.components.vera import CONF_CONTROLLER, DOMAIN +from homeassistant.components.vera.common import ControllerData +from homeassistant.components.vera.config_flow import VeraFlowHandler +from homeassistant.core import HomeAssistant + + +async def test_async_step_import_error(hass: HomeAssistant) -> None: + """Test function.""" + controller = MagicMock(spec=VeraController) # type: VeraController + controller.refresh_data.side_effect = RequestException() + + hass.data[DOMAIN] = ControllerData(controller=controller, devices={}, scenes=()) + + handler = VeraFlowHandler() + handler.hass = hass + handler.async_create_entry = MagicMock( + side_effect=Exception("Should not have been called.") + ) + + result = await handler.async_step_import({CONF_CONTROLLER: "http://127.0.0.1/"}) + + assert result.get("type") == "abort" + assert result.get("reason") == "cannot-connect" + assert result.get("description_placeholders") == {"base_url": "http://127.0.0.1/"} diff --git a/tests/components/vera/test_init.py b/tests/components/vera/test_init.py index baaf6ae56d73aa..91380b04dadfab 100644 --- a/tests/components/vera/test_init.py +++ b/tests/components/vera/test_init.py @@ -1,9 +1,17 @@ """Vera tests.""" from unittest.mock import MagicMock -from pyvera import VeraBinarySensor +from pyvera import VeraBinarySensor, VeraController +from requests.exceptions import RequestException -from homeassistant.components.vera import DOMAIN, async_unload_entry +from homeassistant.components.vera import ( + CONF_CONTROLLER, + DOMAIN, + ControllerData, + async_setup_entry, + async_unload_entry, +) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config @@ -45,13 +53,26 @@ async def test_unload( vera_device1.name = "first_dev" vera_device1.is_tripped = False - component_data = await vera_component_factory.configure_component( + await vera_component_factory.configure_component( hass=hass, controller_config=new_simple_controller_config() ) - component_data.controller_data.controller.stop() config_entries = hass.config_entries.async_entries(DOMAIN) assert config_entries for config_entry in config_entries: await async_unload_entry(hass, config_entry) + + +async def test_async_setup_entry_error(hass: HomeAssistant) -> None: + """Test function.""" + controller = MagicMock(spec=VeraController) # type: VeraController + controller.get_devices.side_effect = RequestException() + controller.get_scenes.side_effect = RequestException() + + hass.data[DOMAIN] = ControllerData(controller=controller, devices={}, scenes=()) + + entry = MagicMock(spec=ConfigEntry) # type: ConfigEntry + entry.data = {CONF_CONTROLLER: "http://127.0.0.1"} + + assert not await async_setup_entry(hass, entry) From eb71881b20665e99e36c8e8b0691fb3e09c2d7fc Mon Sep 17 00:00:00 2001 From: Robbie Van Gorkom Date: Mon, 16 Dec 2019 07:56:20 -0800 Subject: [PATCH 06/21] Fixing pyvera import to make it easier to patch. Addressing PR feedback regarding creation of controller and scheduling of async config flow actions. --- homeassistant/components/vera/__init__.py | 9 +++-- homeassistant/components/vera/config_flow.py | 8 ++--- homeassistant/components/vera/manifest.json | 4 ++- tests/components/vera/common.py | 8 ++--- tests/components/vera/conftest.py | 4 +-- tests/components/vera/test_binary_sensor.py | 4 +-- tests/components/vera/test_climate.py | 12 +++---- tests/components/vera/test_config_flow.py | 4 +-- tests/components/vera/test_cover.py | 6 ++-- tests/components/vera/test_init.py | 22 +++++++----- tests/components/vera/test_light.py | 6 ++-- tests/components/vera/test_lock.py | 6 ++-- tests/components/vera/test_scene.py | 4 +-- tests/components/vera/test_sensor.py | 37 ++++++++------------ tests/components/vera/test_switch.py | 6 ++-- 15 files changed, 68 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index efd6160ae6235a..ee63da973d0668 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -59,9 +59,6 @@ async def async_setup(hass: HomeAssistant, base_config: dict) -> bool: config[CONF_CONTROLLER] = config.get(CONF_CONTROLLER).rstrip("/") base_url = config.get(CONF_CONTROLLER) - controller, _ = veraApi.init_controller(base_url) - hass.data[DOMAIN] = ControllerData(controller=controller, devices=(), scenes=()) - # Build a map of already configured controllers. base_url_entries_map = {} for config_entry in hass.config_entries.async_entries(DOMAIN): @@ -86,15 +83,17 @@ async def async_setup(hass: HomeAssistant, base_config: dict) -> bool: async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Do setup of vera.""" - config = config_entry.data # Get Vera specific configuration. + base_url = config.get(CONF_CONTROLLER) light_ids = config.get(CONF_LIGHTS) exclude_ids = config.get(CONF_EXCLUDE) # Initialize the Vera controller. - controller = hass.data[DOMAIN].controller # type: veraApi.VeraController + controller = veraApi.VeraController(base_url) + controller.start() + hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, lambda event: hass.async_add_executor_job(controller.stop), diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py index c9ffb20a3427fe..6162b4eb49592b 100644 --- a/homeassistant/components/vera/config_flow.py +++ b/homeassistant/components/vera/config_flow.py @@ -1,9 +1,10 @@ """Config flow for Vera.""" +import pyvera as pv from requests.exceptions import RequestException from homeassistant import config_entries -from .const import CONF_CONTROLLER, DOMAIN +from .const import CONF_CONTROLLER, DOMAIN # pylint: disable=unused-import class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -13,10 +14,9 @@ async def async_step_import(self, config: dict): """Handle a flow initialized by import.""" base_url = config.get(CONF_CONTROLLER) - controller = self.hass.data[DOMAIN].controller - try: - controller.refresh_data() + controller = pv.VeraController(base_url) + await self.hass.async_add_job(controller.refresh_data) except RequestException: return self.async_abort( reason="cannot-connect", description_placeholders={"base_url": base_url} diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json index 63102c29687862..948819520bab47 100644 --- a/homeassistant/components/vera/manifest.json +++ b/homeassistant/components/vera/manifest.json @@ -4,5 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/vera", "requirements": ["pyvera==0.3.7"], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@vangorra" + ] } diff --git a/tests/components/vera/common.py b/tests/components/vera/common.py index 28919aff3c5c04..74372e781a4f09 100644 --- a/tests/components/vera/common.py +++ b/tests/components/vera/common.py @@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -SetupCallback = Callable[[pv.VeraController], None] +SetupCallback = Callable[[pv.VeraController, dict], None] ControllerData = NamedTuple( "ControllerData", (("controller", pv.VeraController), ("update_callback", Callable)) @@ -49,9 +49,9 @@ def new_simple_controller_config( class ComponentFactory: """Factory class.""" - def __init__(self, init_controller_mock): + def __init__(self, vera_controller_class_mock): """Initialize the factory.""" - self.init_controller_mock = init_controller_mock + self.vera_controller_class_mock = vera_controller_class_mock async def configure_component( self, hass: HomeAssistant, controller_config: ControllerConfig @@ -81,7 +81,7 @@ async def configure_component( if controller_config.setup_callback: controller_config.setup_callback(controller, hass_config) - self.init_controller_mock.return_value = [controller, 0] + self.vera_controller_class_mock.return_value = controller # Setup Home Assistant. assert await async_setup_component(hass, DOMAIN, hass_config) diff --git a/tests/components/vera/conftest.py b/tests/components/vera/conftest.py index b94a40135d8b09..2c15d3e4182fbe 100644 --- a/tests/components/vera/conftest.py +++ b/tests/components/vera/conftest.py @@ -9,5 +9,5 @@ @pytest.fixture() def vera_component_factory(): """Return a factory for initializing the vera component.""" - with patch("pyvera.init_controller") as init_controller_mock: - yield ComponentFactory(init_controller_mock) + with patch("pyvera.VeraController") as vera_controller_class_mock: + yield ComponentFactory(vera_controller_class_mock) diff --git a/tests/components/vera/test_binary_sensor.py b/tests/components/vera/test_binary_sensor.py index e4ea5197814110..72651d6eda4f74 100644 --- a/tests/components/vera/test_binary_sensor.py +++ b/tests/components/vera/test_binary_sensor.py @@ -1,7 +1,7 @@ """Vera tests.""" from unittest.mock import MagicMock -from pyvera import VeraBinarySensor +import pyvera as pv from homeassistant.core import HomeAssistant @@ -12,7 +12,7 @@ async def test_binary_sensor( hass: HomeAssistant, vera_component_factory: ComponentFactory ) -> None: """Test function.""" - vera_device = MagicMock(spec=VeraBinarySensor) # type: VeraBinarySensor + vera_device = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor vera_device.device_id = 1 vera_device.vera_device_id = 1 vera_device.name = "dev1" diff --git a/tests/components/vera/test_climate.py b/tests/components/vera/test_climate.py index bc8cd3562e1a75..66aa9a2f8a9085 100644 --- a/tests/components/vera/test_climate.py +++ b/tests/components/vera/test_climate.py @@ -1,7 +1,7 @@ """Vera tests.""" from unittest.mock import MagicMock -from pyvera import CATEGORY_THERMOSTAT, VeraController, VeraThermostat +import pyvera as pv from homeassistant.components.climate.const import ( FAN_AUTO, @@ -20,10 +20,10 @@ async def test_climate( hass: HomeAssistant, vera_component_factory: ComponentFactory ) -> None: """Test function.""" - vera_device = MagicMock(spec=VeraThermostat) # type: VeraThermostat + vera_device = MagicMock(spec=pv.VeraThermostat) # type: pv.VeraThermostat vera_device.device_id = 1 vera_device.name = "dev1" - vera_device.category = CATEGORY_THERMOSTAT + vera_device.category = pv.CATEGORY_THERMOSTAT vera_device.power = 10 vera_device.get_current_temperature.return_value = 71 vera_device.get_hvac_mode.return_value = "Off" @@ -123,17 +123,17 @@ async def test_climate_f( hass: HomeAssistant, vera_component_factory: ComponentFactory ) -> None: """Test function.""" - vera_device = MagicMock(spec=VeraThermostat) # type: VeraThermostat + vera_device = MagicMock(spec=pv.VeraThermostat) # type: pv.VeraThermostat vera_device.device_id = 1 vera_device.name = "dev1" - vera_device.category = CATEGORY_THERMOSTAT + vera_device.category = pv.CATEGORY_THERMOSTAT vera_device.power = 10 vera_device.get_current_temperature.return_value = 71 vera_device.get_hvac_mode.return_value = "Off" vera_device.get_current_goal_temperature.return_value = 72 entity_id = "climate.dev1_1" - def setup_callback(controller: VeraController, hass_config: dict) -> None: + def setup_callback(controller: pv.VeraController, hass_config: dict) -> None: controller.temperature_units = "F" component_data = await vera_component_factory.configure_component( diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index cb1cacb2afc42b..15c7c24c5c4783 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -1,7 +1,7 @@ """Vera tests.""" from unittest.mock import MagicMock -from pyvera import VeraController +import pyvera as pv from requests.exceptions import RequestException from homeassistant.components.vera import CONF_CONTROLLER, DOMAIN @@ -12,7 +12,7 @@ async def test_async_step_import_error(hass: HomeAssistant) -> None: """Test function.""" - controller = MagicMock(spec=VeraController) # type: VeraController + controller = MagicMock(spec=pv.VeraController) # type: pv.VeraController controller.refresh_data.side_effect = RequestException() hass.data[DOMAIN] = ControllerData(controller=controller, devices={}, scenes=()) diff --git a/tests/components/vera/test_cover.py b/tests/components/vera/test_cover.py index 3414419aa9f2c3..62cd47f831cdc3 100644 --- a/tests/components/vera/test_cover.py +++ b/tests/components/vera/test_cover.py @@ -1,7 +1,7 @@ """Vera tests.""" from unittest.mock import MagicMock -from pyvera import CATEGORY_CURTAIN, VeraCurtain +import pyvera as pv from homeassistant.core import HomeAssistant @@ -12,10 +12,10 @@ async def test_cover( hass: HomeAssistant, vera_component_factory: ComponentFactory ) -> None: """Test function.""" - vera_device = MagicMock(spec=VeraCurtain) # type: VeraCurtain + vera_device = MagicMock(spec=pv.VeraCurtain) # type: pv.VeraCurtain vera_device.device_id = 1 vera_device.name = "dev1" - vera_device.category = CATEGORY_CURTAIN + vera_device.category = pv.CATEGORY_CURTAIN vera_device.is_closed = False vera_device.get_level.return_value = 0 entity_id = "cover.dev1_1" diff --git a/tests/components/vera/test_init.py b/tests/components/vera/test_init.py index 91380b04dadfab..fc84e5209dccf2 100644 --- a/tests/components/vera/test_init.py +++ b/tests/components/vera/test_init.py @@ -1,13 +1,12 @@ """Vera tests.""" from unittest.mock import MagicMock -from pyvera import VeraBinarySensor, VeraController +import pyvera as pv from requests.exceptions import RequestException from homeassistant.components.vera import ( CONF_CONTROLLER, DOMAIN, - ControllerData, async_setup_entry, async_unload_entry, ) @@ -21,7 +20,7 @@ async def test_init( hass: HomeAssistant, vera_component_factory: ComponentFactory ) -> None: """Test function.""" - vera_device1 = MagicMock(spec=VeraBinarySensor) # type: VeraBinarySensor + vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor vera_device1.device_id = 1 vera_device1.vera_device_id = 1 vera_device1.name = "first_dev" @@ -47,7 +46,7 @@ async def test_unload( hass: HomeAssistant, vera_component_factory: ComponentFactory ) -> None: """Test function.""" - vera_device1 = MagicMock(spec=VeraBinarySensor) # type: VeraBinarySensor + vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor vera_device1.device_id = 1 vera_device1.vera_device_id = 1 vera_device1.name = "first_dev" @@ -64,14 +63,19 @@ async def test_unload( await async_unload_entry(hass, config_entry) -async def test_async_setup_entry_error(hass: HomeAssistant) -> None: +async def test_async_setup_entry_error( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: """Test function.""" - controller = MagicMock(spec=VeraController) # type: VeraController - controller.get_devices.side_effect = RequestException() - controller.get_scenes.side_effect = RequestException() - hass.data[DOMAIN] = ControllerData(controller=controller, devices={}, scenes=()) + def setup_callback(controller: pv.VeraController, config: dict) -> None: + controller.get_devices.side_effect = RequestException() + controller.get_scenes.side_effect = RequestException() + await vera_component_factory.configure_component( + hass=hass, + controller_config=new_simple_controller_config(setup_callback=setup_callback), + ) entry = MagicMock(spec=ConfigEntry) # type: ConfigEntry entry.data = {CONF_CONTROLLER: "http://127.0.0.1"} diff --git a/tests/components/vera/test_light.py b/tests/components/vera/test_light.py index 0501a7dc789417..fefa07ffa6ee40 100644 --- a/tests/components/vera/test_light.py +++ b/tests/components/vera/test_light.py @@ -1,7 +1,7 @@ """Vera tests.""" from unittest.mock import MagicMock -from pyvera import CATEGORY_DIMMER, VeraDimmer +import pyvera as pv from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_HS_COLOR from homeassistant.core import HomeAssistant @@ -13,10 +13,10 @@ async def test_light( hass: HomeAssistant, vera_component_factory: ComponentFactory ) -> None: """Test function.""" - vera_device = MagicMock(spec=VeraDimmer) # type: VeraDimmer + vera_device = MagicMock(spec=pv.VeraDimmer) # type: pv.VeraDimmer vera_device.device_id = 1 vera_device.name = "dev1" - vera_device.category = CATEGORY_DIMMER + vera_device.category = pv.CATEGORY_DIMMER vera_device.is_switched_on = MagicMock(return_value=False) vera_device.get_brightness = MagicMock(return_value=0) vera_device.get_color = MagicMock(return_value=[0, 0, 0]) diff --git a/tests/components/vera/test_lock.py b/tests/components/vera/test_lock.py index 95be83cc4d042d..d1b2209294a375 100644 --- a/tests/components/vera/test_lock.py +++ b/tests/components/vera/test_lock.py @@ -1,7 +1,7 @@ """Vera tests.""" from unittest.mock import MagicMock -from pyvera import CATEGORY_LOCK, VeraLock +import pyvera as pv from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED from homeassistant.core import HomeAssistant @@ -13,10 +13,10 @@ async def test_lock( hass: HomeAssistant, vera_component_factory: ComponentFactory ) -> None: """Test function.""" - vera_device = MagicMock(spec=VeraLock) # type: VeraLock + vera_device = MagicMock(spec=pv.VeraLock) # type: pv.VeraLock vera_device.device_id = 1 vera_device.name = "dev1" - vera_device.category = CATEGORY_LOCK + vera_device.category = pv.CATEGORY_LOCK vera_device.is_locked = MagicMock(return_value=False) entity_id = "lock.dev1_1" diff --git a/tests/components/vera/test_scene.py b/tests/components/vera/test_scene.py index d32f639cc24e53..732a331681bdb2 100644 --- a/tests/components/vera/test_scene.py +++ b/tests/components/vera/test_scene.py @@ -1,7 +1,7 @@ """Vera tests.""" from unittest.mock import MagicMock -from pyvera import VeraScene +import pyvera as pv from homeassistant.core import HomeAssistant @@ -12,7 +12,7 @@ async def test_scene( hass: HomeAssistant, vera_component_factory: ComponentFactory ) -> None: """Test function.""" - vera_scene = MagicMock(spec=VeraScene) # type: VeraScene + vera_scene = MagicMock(spec=pv.VeraScene) # type: pv.VeraScene vera_scene.scene_id = 1 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 d8b2a27b0de2b7..cd2e0613970206 100644 --- a/tests/components/vera/test_sensor.py +++ b/tests/components/vera/test_sensor.py @@ -2,16 +2,7 @@ from typing import Any, Callable, Tuple from unittest.mock import MagicMock -from pyvera import ( - CATEGORY_HUMIDITY_SENSOR, - CATEGORY_LIGHT_SENSOR, - CATEGORY_POWER_METER, - CATEGORY_SCENE_CONTROLLER, - CATEGORY_TEMPERATURE_SENSOR, - CATEGORY_UV_SENSOR, - VeraController, - VeraSensor, -) +import pyvera as pv from homeassistant.const import UNIT_PERCENTAGE from homeassistant.core import HomeAssistant @@ -26,10 +17,10 @@ async def run_sensor_test( class_property: str, assert_states: Tuple[Tuple[Any, Any]], assert_unit_of_measurement: str = None, - setup_callback: Callable[[VeraController], None] = None, + setup_callback: Callable[[pv.VeraController], None] = None, ) -> None: """Test generic sensor.""" - vera_device = MagicMock(spec=VeraSensor) # type: VeraSensor + vera_device = MagicMock(spec=pv.VeraSensor) # type: pv.VeraSensor vera_device.device_id = 1 vera_device.name = "dev1" vera_device.category = category @@ -59,13 +50,13 @@ async def test_temperature_sensor_f( ) -> None: """Test function.""" - def setup_callback(controller: VeraController, hass_config: dict) -> None: + def setup_callback(controller: pv.VeraController, hass_config: dict) -> None: controller.temperature_units = "F" await run_sensor_test( hass=hass, vera_component_factory=vera_component_factory, - category=CATEGORY_TEMPERATURE_SENSOR, + category=pv.CATEGORY_TEMPERATURE_SENSOR, class_property="temperature", assert_states=(("33", "1"), ("44", "7")), setup_callback=setup_callback, @@ -79,7 +70,7 @@ async def test_temperature_sensor_c( await run_sensor_test( hass=hass, vera_component_factory=vera_component_factory, - category=CATEGORY_TEMPERATURE_SENSOR, + category=pv.CATEGORY_TEMPERATURE_SENSOR, class_property="temperature", assert_states=(("33", "33"), ("44", "44")), ) @@ -92,7 +83,7 @@ async def test_light_sensor( await run_sensor_test( hass=hass, vera_component_factory=vera_component_factory, - category=CATEGORY_LIGHT_SENSOR, + category=pv.CATEGORY_LIGHT_SENSOR, class_property="light", assert_states=(("12", "12"), ("13", "13")), assert_unit_of_measurement="lx", @@ -106,7 +97,7 @@ async def test_uv_sensor( await run_sensor_test( hass=hass, vera_component_factory=vera_component_factory, - category=CATEGORY_UV_SENSOR, + category=pv.CATEGORY_UV_SENSOR, class_property="light", assert_states=(("12", "12"), ("13", "13")), assert_unit_of_measurement="level", @@ -120,7 +111,7 @@ async def test_humidity_sensor( await run_sensor_test( hass=hass, vera_component_factory=vera_component_factory, - category=CATEGORY_HUMIDITY_SENSOR, + category=pv.CATEGORY_HUMIDITY_SENSOR, class_property="humidity", assert_states=(("12", "12"), ("13", "13")), assert_unit_of_measurement=UNIT_PERCENTAGE, @@ -134,7 +125,7 @@ async def test_power_meter_sensor( await run_sensor_test( hass=hass, vera_component_factory=vera_component_factory, - category=CATEGORY_POWER_METER, + category=pv.CATEGORY_POWER_METER, class_property="power", assert_states=(("12", "12"), ("13", "13")), assert_unit_of_measurement="watts", @@ -146,7 +137,7 @@ async def test_trippable_sensor( ) -> None: """Test function.""" - def setup_callback(controller: VeraController, hass_config: dict) -> None: + def setup_callback(controller: pv.VeraController, hass_config: dict) -> None: controller.get_devices()[0].is_trippable = True await run_sensor_test( @@ -164,7 +155,7 @@ async def test_unknown_sensor( ) -> None: """Test function.""" - def setup_callback(controller: VeraController, hass_config: dict) -> None: + def setup_callback(controller: pv.VeraController, hass_config: dict) -> None: controller.get_devices()[0].is_trippable = False await run_sensor_test( @@ -181,10 +172,10 @@ async def test_scene_controller_sensor( hass: HomeAssistant, vera_component_factory: ComponentFactory ) -> None: """Test function.""" - vera_device = MagicMock(spec=VeraSensor) # type: VeraSensor + vera_device = MagicMock(spec=pv.VeraSensor) # type: pv.VeraSensor vera_device.device_id = 1 vera_device.name = "dev1" - vera_device.category = CATEGORY_SCENE_CONTROLLER + vera_device.category = pv.CATEGORY_SCENE_CONTROLLER vera_device.get_last_scene_id = MagicMock(return_value="id0") vera_device.get_last_scene_time = MagicMock(return_value="0000") entity_id = "sensor.dev1_1" diff --git a/tests/components/vera/test_switch.py b/tests/components/vera/test_switch.py index a80928366a1540..c41afad4759f8f 100644 --- a/tests/components/vera/test_switch.py +++ b/tests/components/vera/test_switch.py @@ -1,7 +1,7 @@ """Vera tests.""" from unittest.mock import MagicMock -from pyvera import CATEGORY_SWITCH, VeraSwitch +import pyvera as pv from homeassistant.core import HomeAssistant @@ -12,10 +12,10 @@ async def test_switch( hass: HomeAssistant, vera_component_factory: ComponentFactory ) -> None: """Test function.""" - vera_device = MagicMock(spec=VeraSwitch) # type: VeraSwitch + vera_device = MagicMock(spec=pv.VeraSwitch) # type: pv.VeraSwitch vera_device.device_id = 1 vera_device.name = "dev1" - vera_device.category = CATEGORY_SWITCH + vera_device.category = pv.CATEGORY_SWITCH vera_device.is_switched_on = MagicMock(return_value=False) entity_id = "switch.dev1_1" From 251ba61d8e399c58ac75d2ba0c7e342c8adb57c2 Mon Sep 17 00:00:00 2001 From: Robbie Van Gorkom Date: Mon, 16 Dec 2019 13:06:03 -0800 Subject: [PATCH 07/21] Updating code owners file. --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index dee1a510e2eee9..8443d0a633a43c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -407,6 +407,7 @@ homeassistant/components/usgs_earthquakes_feed/* @exxamalte homeassistant/components/utility_meter/* @dgomes homeassistant/components/velbus/* @Cereal2nd @brefra homeassistant/components/velux/* @Julius2342 +homeassistant/components/vera/* @vangorra homeassistant/components/versasense/* @flamm3blemuff1n homeassistant/components/version/* @fabaff homeassistant/components/vesync/* @markperdue @webdjoe From d6e478446accd5d1fa321feb1b4c7b708790a86f Mon Sep 17 00:00:00 2001 From: Robbie Van Gorkom Date: Mon, 16 Dec 2019 20:27:11 -0800 Subject: [PATCH 08/21] Small fixes. --- homeassistant/components/vera/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index ee63da973d0668..93b060f0203d4f 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -55,6 +55,9 @@ async def async_setup(hass: HomeAssistant, base_config: dict) -> bool: """Set up for Vera controllers.""" config = base_config.get(DOMAIN) + if not config: + return True + # Normalize the base url. config[CONF_CONTROLLER] = config.get(CONF_CONTROLLER).rstrip("/") base_url = config.get(CONF_CONTROLLER) @@ -100,9 +103,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): ) try: - all_devices = controller.get_devices() + all_devices = await hass.async_add_executor_job(controller.get_devices) - all_scenes = controller.get_scenes() + all_scenes = await hass.async_add_executor_job(controller.get_scenes) except RequestException: # There was a network related error connecting to the Vera controller. _LOGGER.exception("Error communicating with Vera API") From 30eb170d197b8df700545d7a06db22598637925f Mon Sep 17 00:00:00 2001 From: Robbie Van Gorkom Date: Tue, 7 Jan 2020 08:27:14 -0800 Subject: [PATCH 09/21] Adding a user config flow step. --- homeassistant/components/vera/config_flow.py | 26 +++++- homeassistant/components/vera/manifest.json | 1 + homeassistant/components/vera/strings.json | 12 ++- homeassistant/generated/config_flows.py | 1 + tests/components/vera/test_config_flow.py | 95 +++++++++++++++++--- 5 files changed, 118 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py index 6162b4eb49592b..a1474c793596aa 100644 --- a/homeassistant/components/vera/config_flow.py +++ b/homeassistant/components/vera/config_flow.py @@ -1,17 +1,39 @@ """Config flow for Vera.""" import pyvera as pv from requests.exceptions import RequestException +import voluptuous as vol from homeassistant import config_entries +from homeassistant.helpers import config_validation as cv -from .const import CONF_CONTROLLER, DOMAIN # pylint: disable=unused-import +from .const import CONF_CONTROLLER, DOMAIN class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Vera config flow.""" + async def async_step_user(self, config: dict = None): + """Handle user initiated flow.""" + if self.hass.config_entries.async_entries(DOMAIN): + return self.async_abort(reason="already_setup") + + if config: + return await self.async_step_finish(config) + + return self.async_show_form( + step_id="setup", + data_schema=vol.Schema({vol.Required(CONF_CONTROLLER): cv.url}), + ) + async def async_step_import(self, config: dict): """Handle a flow initialized by import.""" + if self.hass.config_entries.async_entries(DOMAIN): + return self.async_abort(reason="already_setup") + + return await self.async_step_finish(config) + + async def async_step_finish(self, config: dict): + """Validate and create config entry.""" base_url = config.get(CONF_CONTROLLER) try: @@ -19,7 +41,7 @@ async def async_step_import(self, config: dict): await self.hass.async_add_job(controller.refresh_data) except RequestException: return self.async_abort( - reason="cannot-connect", description_placeholders={"base_url": base_url} + reason="cannot_connect", description_placeholders={"base_url": base_url} ) return self.async_create_entry(title=base_url, data=config) diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json index 948819520bab47..4f585d964a86a7 100644 --- a/homeassistant/components/vera/manifest.json +++ b/homeassistant/components/vera/manifest.json @@ -1,6 +1,7 @@ { "domain": "vera", "name": "Vera", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/vera", "requirements": ["pyvera==0.3.7"], "dependencies": [], diff --git a/homeassistant/components/vera/strings.json b/homeassistant/components/vera/strings.json index 97ac9d0415ae9f..d2e59da8a821b7 100644 --- a/homeassistant/components/vera/strings.json +++ b/homeassistant/components/vera/strings.json @@ -2,7 +2,17 @@ "config": { "title": "Vera", "abort": { - "cannot-connect": "Could not connect to controller with url {base_url}" + "already_setup": "A controller is already configured.", + "cannot_connect": "Could not connect to controller with url {base_url}" + }, + "step": { + "setup": { + "title": "Setup Vera controller", + "description": "Provide a Vera controller url below. It should look like this: http://192.168.1.161:3480/", + "data": { + "vera_controller_url": "Controller URL" + } + } } } } diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 05bc4a7ba4a306..a2df556abf26fa 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -119,6 +119,7 @@ "unifi", "upnp", "velbus", + "vera", "vesync", "vilfo", "vizio", diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index 15c7c24c5c4783..8cbb6f16d77425 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -1,30 +1,97 @@ """Vera tests.""" from unittest.mock import MagicMock -import pyvera as pv +from mock import patch from requests.exceptions import RequestException -from homeassistant.components.vera import CONF_CONTROLLER, DOMAIN -from homeassistant.components.vera.common import ControllerData +from homeassistant.components.vera import CONF_CONTROLLER from homeassistant.components.vera.config_flow import VeraFlowHandler from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) -async def test_async_step_import_error(hass: HomeAssistant) -> None: +async def test_aync_step_user_success(hass: HomeAssistant) -> None: """Test function.""" - controller = MagicMock(spec=pv.VeraController) # type: pv.VeraController - controller.refresh_data.side_effect = RequestException() + with patch("pyvera.VeraController") as vera_controller_class_mock: + controller = MagicMock() + controller.refresh_data = MagicMock() + vera_controller_class_mock.return_value = controller - hass.data[DOMAIN] = ControllerData(controller=controller, devices={}, scenes=()) + handler = VeraFlowHandler() + handler.hass = hass + base_url = "http://127.0.0.1/" + result = await handler.async_step_user() + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == "setup" + + result = await handler.async_step_user({CONF_CONTROLLER: base_url}) + assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("title") == base_url + assert result.get("data") == {CONF_CONTROLLER: base_url} + + +async def test_async_step_user_alredy_setup(hass: HomeAssistant) -> None: + """Test function.""" handler = VeraFlowHandler() handler.hass = hass - handler.async_create_entry = MagicMock( - side_effect=Exception("Should not have been called.") - ) - result = await handler.async_step_import({CONF_CONTROLLER: "http://127.0.0.1/"}) + with patch.object(hass.config_entries, "async_entries", return_value=[{}]): + result = await handler.async_step_user() + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "already_setup" + + +async def test_aync_step_import_success(hass: HomeAssistant) -> None: + """Test function.""" + with patch("pyvera.VeraController") as vera_controller_class_mock: + controller = MagicMock() + controller.refresh_data = MagicMock() + vera_controller_class_mock.return_value = controller + + handler = VeraFlowHandler() + handler.hass = hass + base_url = "http://127.0.0.1/" + + result = await handler.async_step_import({CONF_CONTROLLER: base_url}) + + assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("title") == base_url + assert result.get("data") == {CONF_CONTROLLER: base_url} + + +async def test_async_step_import_alredy_setup(hass: HomeAssistant) -> None: + """Test function.""" + handler = VeraFlowHandler() + handler.hass = hass + + with patch.object(hass.config_entries, "async_entries", return_value=[{}]): + result = await handler.async_step_import({}) + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "already_setup" + + +async def test_async_step_finish_error(hass: HomeAssistant) -> None: + """Test function.""" + with patch("pyvera.VeraController") as vera_controller_class_mock: + controller = MagicMock() + controller.refresh_data = MagicMock(side_effect=RequestException()) + vera_controller_class_mock.return_value = controller + + handler = VeraFlowHandler() + handler.hass = hass + handler.async_create_entry = MagicMock( + side_effect=Exception("Should not have been called.") + ) + + result = await handler.async_step_finish({CONF_CONTROLLER: "http://127.0.0.1/"}) - assert result.get("type") == "abort" - assert result.get("reason") == "cannot-connect" - assert result.get("description_placeholders") == {"base_url": "http://127.0.0.1/"} + assert result.get("type") == "abort" + assert result.get("reason") == "cannot_connect" + assert result.get("description_placeholders") == { + "base_url": "http://127.0.0.1/" + } From 29aeab7786b8b42869a885104569585eaa039f62 Mon Sep 17 00:00:00 2001 From: Robbie Van Gorkom Date: Tue, 7 Jan 2020 09:03:27 -0800 Subject: [PATCH 10/21] Adding optional configs for user config flow. --- homeassistant/components/vera/config_flow.py | 29 +++++++++++++++++--- homeassistant/components/vera/strings.json | 6 ++-- tests/components/vera/test_config_flow.py | 19 ++++++++++--- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py index a1474c793596aa..9ee49c83c25a1b 100644 --- a/homeassistant/components/vera/config_flow.py +++ b/homeassistant/components/vera/config_flow.py @@ -1,13 +1,23 @@ """Config flow for Vera.""" +import re +from typing import List + import pyvera as pv from requests.exceptions import RequestException import voluptuous as vol from homeassistant import config_entries -from homeassistant.helpers import config_validation as cv +from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS from .const import CONF_CONTROLLER, DOMAIN +LIST_REGEX = re.compile("[^0-9]+") + + +def parse_int_list(data: str) -> List[int]: + """Parse a string into a list of ints.""" + return [int(s) for s in LIST_REGEX.split(data) if len(s) > 0] + class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Vera config flow.""" @@ -18,11 +28,22 @@ async def async_step_user(self, config: dict = None): return self.async_abort(reason="already_setup") if config: - return await self.async_step_finish(config) + new_config = { + CONF_CONTROLLER: config.get(CONF_CONTROLLER), + CONF_LIGHTS: parse_int_list(config.get(CONF_LIGHTS, "")), + CONF_EXCLUDE: parse_int_list(config.get(CONF_EXCLUDE, "")), + } + return await self.async_step_finish(new_config) return self.async_show_form( - step_id="setup", - data_schema=vol.Schema({vol.Required(CONF_CONTROLLER): cv.url}), + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_CONTROLLER): str, + vol.Optional(CONF_LIGHTS): str, + vol.Optional(CONF_EXCLUDE): str, + } + ), ) async def async_step_import(self, config: dict): diff --git a/homeassistant/components/vera/strings.json b/homeassistant/components/vera/strings.json index d2e59da8a821b7..7a3eb1e4bd1341 100644 --- a/homeassistant/components/vera/strings.json +++ b/homeassistant/components/vera/strings.json @@ -6,11 +6,13 @@ "cannot_connect": "Could not connect to controller with url {base_url}" }, "step": { - "setup": { + "user": { "title": "Setup Vera controller", "description": "Provide a Vera controller url below. It should look like this: http://192.168.1.161:3480/", "data": { - "vera_controller_url": "Controller URL" + "vera_controller_url": "Controller URL", + "lights": "Optional: Delimited list of Vera device ids (ints).", + "exclude": "Optional: Delimited list of Vera device ids (ints)." } } } diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index 8cbb6f16d77425..5eac6a83a780e0 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -6,6 +6,7 @@ from homeassistant.components.vera import CONF_CONTROLLER from homeassistant.components.vera.config_flow import VeraFlowHandler +from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -27,12 +28,22 @@ async def test_aync_step_user_success(hass: HomeAssistant) -> None: result = await handler.async_step_user() assert result.get("type") == RESULT_TYPE_FORM - assert result.get("step_id") == "setup" - - result = await handler.async_step_user({CONF_CONTROLLER: base_url}) + assert result.get("step_id") == "user" + + result = await handler.async_step_user( + { + CONF_CONTROLLER: base_url, + CONF_LIGHTS: "1,2;3 4 5_6bb7", + CONF_EXCLUDE: "8,9;10 11 12_13bb14", + } + ) assert result.get("type") == RESULT_TYPE_CREATE_ENTRY assert result.get("title") == base_url - assert result.get("data") == {CONF_CONTROLLER: base_url} + assert result.get("data") == { + CONF_CONTROLLER: base_url, + CONF_LIGHTS: [1, 2, 3, 4, 5, 6, 7], + CONF_EXCLUDE: [8, 9, 10, 11, 12, 13, 14], + } async def test_async_step_user_alredy_setup(hass: HomeAssistant) -> None: From 232fb180939e513c4cef47b85f4ca6e47a0dc0b1 Mon Sep 17 00:00:00 2001 From: Robbie Van Gorkom Date: Tue, 7 Jan 2020 09:27:30 -0800 Subject: [PATCH 11/21] Updating strings to be more clear to the user. --- homeassistant/components/vera/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vera/strings.json b/homeassistant/components/vera/strings.json index 7a3eb1e4bd1341..994e5ec16c1061 100644 --- a/homeassistant/components/vera/strings.json +++ b/homeassistant/components/vera/strings.json @@ -8,11 +8,11 @@ "step": { "user": { "title": "Setup Vera controller", - "description": "Provide a Vera controller url below. It should look like this: http://192.168.1.161:3480/", + "description": "Provide a Vera controller url below. It should look like this: http://192.168.1.161:3480/.\n\nSee the vera documentation for details on optional parameters: https://www.home-assistant.io/integrations/vera/", "data": { "vera_controller_url": "Controller URL", - "lights": "Optional: Delimited list of Vera device ids (ints).", - "exclude": "Optional: Delimited list of Vera device ids (ints)." + "lights": "Optional: Light ids. Delimited list of Vera device ids (ints).", + "exclude": "Optional: Exclude ids. Delimited list of Vera device ids (ints)." } } } From d590c8dd53b757c9b4b15147e83174a049d99c85 Mon Sep 17 00:00:00 2001 From: Robbie Van Gorkom Date: Tue, 7 Jan 2020 19:17:23 -0800 Subject: [PATCH 12/21] Adding options flow. Fixing some PR feedback. --- homeassistant/components/vera/__init__.py | 24 +---- homeassistant/components/vera/config_flow.py | 56 +++++++---- homeassistant/components/vera/strings.json | 18 +++- tests/components/vera/test_config_flow.py | 99 +++++++++++++------- 4 files changed, 121 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 93b060f0203d4f..99f61f26e15e30 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -55,26 +55,10 @@ async def async_setup(hass: HomeAssistant, base_config: dict) -> bool: """Set up for Vera controllers.""" config = base_config.get(DOMAIN) - if not config: + if config is None: return True - # Normalize the base url. - config[CONF_CONTROLLER] = config.get(CONF_CONTROLLER).rstrip("/") - base_url = config.get(CONF_CONTROLLER) - - # Build a map of already configured controllers. - base_url_entries_map = {} - for config_entry in hass.config_entries.async_entries(DOMAIN): - base_url = config_entry.data.get(CONF_CONTROLLER) - base_url_entries_map[base_url] = config_entry - - entry = base_url_entries_map.get(base_url) - if entry: - _LOGGER.debug("Updating existing config for %s", base_url) - hass.config_entries.async_update_entry(entry=entry, data=config) - return True - - _LOGGER.debug("Creating new config for %s", base_url) + _LOGGER.debug("Creating new config for %s", config.get(CONF_CONTROLLER)) hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config, @@ -90,8 +74,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): # Get Vera specific configuration. base_url = config.get(CONF_CONTROLLER) - light_ids = config.get(CONF_LIGHTS) - exclude_ids = config.get(CONF_EXCLUDE) + light_ids = config_entry.options.get(CONF_LIGHTS, []) + exclude_ids = config_entry.options.get(CONF_EXCLUDE, []) # Initialize the Vera controller. controller = veraApi.VeraController(base_url) diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py index 9ee49c83c25a1b..3ac963234d4b97 100644 --- a/homeassistant/components/vera/config_flow.py +++ b/homeassistant/components/vera/config_flow.py @@ -8,6 +8,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS +from homeassistant.core import callback from .const import CONF_CONTROLLER, DOMAIN @@ -19,31 +20,48 @@ def parse_int_list(data: str) -> List[int]: return [int(s) for s in LIST_REGEX.split(data) if len(s) > 0] +class OptionsFlowHandler(config_entries.OptionsFlow): + """Options for the component.""" + + async def async_step_init(self, user_input=None): + """Manage the options.""" + if user_input is not None: + return self.async_create_entry( + title="", + data={ + CONF_LIGHTS: parse_int_list(user_input.get(CONF_LIGHTS, "")), + CONF_EXCLUDE: parse_int_list(user_input.get(CONF_EXCLUDE, "")), + }, + ) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + {vol.Optional(CONF_LIGHTS): str, vol.Optional(CONF_EXCLUDE): str} + ), + ) + + class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Vera config flow.""" - async def async_step_user(self, config: dict = None): + @staticmethod + @callback + def async_get_options_flow(config_entry) -> OptionsFlowHandler: + """Get the options flow.""" + return OptionsFlowHandler() + + 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_setup") - if config: - new_config = { - CONF_CONTROLLER: config.get(CONF_CONTROLLER), - CONF_LIGHTS: parse_int_list(config.get(CONF_LIGHTS, "")), - CONF_EXCLUDE: parse_int_list(config.get(CONF_EXCLUDE, "")), - } - return await self.async_step_finish(new_config) + if user_input is not None: + return await self.async_step_finish(user_input) return self.async_show_form( step_id="user", - data_schema=vol.Schema( - { - vol.Required(CONF_CONTROLLER): str, - vol.Optional(CONF_LIGHTS): str, - vol.Optional(CONF_EXCLUDE): str, - } - ), + data_schema=vol.Schema({vol.Required(CONF_CONTROLLER): str}), ) async def async_step_import(self, config: dict): @@ -55,14 +73,18 @@ async def async_step_import(self, config: dict): async def async_step_finish(self, config: dict): """Validate and create config entry.""" - base_url = config.get(CONF_CONTROLLER) + base_url = config[CONF_CONTROLLER] = config.get(CONF_CONTROLLER).rstrip("/") + controller = pv.VeraController(base_url) try: - controller = pv.VeraController(base_url) await self.hass.async_add_job(controller.refresh_data) except RequestException: return self.async_abort( reason="cannot_connect", description_placeholders={"base_url": base_url} ) + await self.async_set_unique_id( + controller.serial_number, raise_on_progress=False + ) + return self.async_create_entry(title=base_url, data=config) diff --git a/homeassistant/components/vera/strings.json b/homeassistant/components/vera/strings.json index 994e5ec16c1061..f7ef36080e7cf1 100644 --- a/homeassistant/components/vera/strings.json +++ b/homeassistant/components/vera/strings.json @@ -8,11 +8,21 @@ "step": { "user": { "title": "Setup Vera controller", - "description": "Provide a Vera controller url below. It should look like this: http://192.168.1.161:3480/.\n\nSee the vera documentation for details on optional parameters: https://www.home-assistant.io/integrations/vera/", + "description": "Provide a Vera controller url below. It should look like this: http://192.168.1.161:3480.", "data": { - "vera_controller_url": "Controller URL", - "lights": "Optional: Light ids. Delimited list of Vera device ids (ints).", - "exclude": "Optional: Exclude ids. Delimited list of Vera device ids (ints)." + "vera_controller_url": "Controller URL" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "Vera controller options", + "description": "See the vera documentation for details on optional parameters: https://www.home-assistant.io/integrations/vera/", + "data": { + "lights": "Vera switch device ids to treat as lights in Home Assistant.", + "exclude": "Vera device ids to exclude from Home Assistant." } } } diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index 5eac6a83a780e0..a4bc9a651d900b 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -4,7 +4,8 @@ from mock import patch from requests.exceptions import RequestException -from homeassistant.components.vera import CONF_CONTROLLER +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.vera import CONF_CONTROLLER, DOMAIN from homeassistant.components.vera.config_flow import VeraFlowHandler from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS from homeassistant.core import HomeAssistant @@ -14,45 +15,46 @@ RESULT_TYPE_FORM, ) +from tests.common import MockConfigEntry + async def test_aync_step_user_success(hass: HomeAssistant) -> None: """Test function.""" with patch("pyvera.VeraController") as vera_controller_class_mock: controller = MagicMock() controller.refresh_data = MagicMock() + controller.serial_number = "serial_number_0" vera_controller_class_mock.return_value = controller - handler = VeraFlowHandler() - handler.hass = hass - base_url = "http://127.0.0.1/" - - result = await handler.async_step_user() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) assert result.get("type") == RESULT_TYPE_FORM - assert result.get("step_id") == "user" - - result = await handler.async_step_user( - { - CONF_CONTROLLER: base_url, - CONF_LIGHTS: "1,2;3 4 5_6bb7", - CONF_EXCLUDE: "8,9;10 11 12_13bb14", - } + assert result.get("step_id") == config_entries.SOURCE_USER + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, ) assert result.get("type") == RESULT_TYPE_CREATE_ENTRY - assert result.get("title") == base_url + assert result.get("title") == "http://127.0.0.1:123" assert result.get("data") == { - CONF_CONTROLLER: base_url, - CONF_LIGHTS: [1, 2, 3, 4, 5, 6, 7], - CONF_EXCLUDE: [8, 9, 10, 11, 12, 13, 14], + CONF_CONTROLLER: "http://127.0.0.1:123", } + assert result.get("result").unique_id == controller.serial_number -async def test_async_step_user_alredy_setup(hass: HomeAssistant) -> None: +async def test_async_step_user_already_setup(hass: HomeAssistant) -> None: """Test function.""" handler = VeraFlowHandler() + handler.context = {} handler.hass = hass with patch.object(hass.config_entries, "async_entries", return_value=[{}]): - result = await handler.async_step_user() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) assert result.get("type") == RESULT_TYPE_ABORT assert result.get("reason") == "already_setup" @@ -62,26 +64,31 @@ async def test_aync_step_import_success(hass: HomeAssistant) -> None: with patch("pyvera.VeraController") as vera_controller_class_mock: controller = MagicMock() controller.refresh_data = MagicMock() + controller.serial_number = "serial_number_1" vera_controller_class_mock.return_value = controller - handler = VeraFlowHandler() - handler.hass = hass - base_url = "http://127.0.0.1/" - - result = await handler.async_step_import({CONF_CONTROLLER: base_url}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, + ) assert result.get("type") == RESULT_TYPE_CREATE_ENTRY - assert result.get("title") == base_url - assert result.get("data") == {CONF_CONTROLLER: base_url} + assert result.get("title") == "http://127.0.0.1:123" + assert result.get("data") == {CONF_CONTROLLER: "http://127.0.0.1:123"} + assert result.get("result").unique_id == controller.serial_number async def test_async_step_import_alredy_setup(hass: HomeAssistant) -> None: """Test function.""" handler = VeraFlowHandler() + handler.context = {} handler.hass = hass with patch.object(hass.config_entries, "async_entries", return_value=[{}]): - result = await handler.async_step_import({}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={} + ) assert result.get("type") == RESULT_TYPE_ABORT assert result.get("reason") == "already_setup" @@ -93,16 +100,38 @@ async def test_async_step_finish_error(hass: HomeAssistant) -> None: controller.refresh_data = MagicMock(side_effect=RequestException()) vera_controller_class_mock.return_value = controller - handler = VeraFlowHandler() - handler.hass = hass - handler.async_create_entry = MagicMock( - side_effect=Exception("Should not have been called.") + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, ) - result = await handler.async_step_finish({CONF_CONTROLLER: "http://127.0.0.1/"}) - assert result.get("type") == "abort" assert result.get("reason") == "cannot_connect" assert result.get("description_placeholders") == { - "base_url": "http://127.0.0.1/" + "base_url": "http://127.0.0.1:123" } + + +async def test_options(hass): + """Test updating options.""" + base_url = "http://127.0.0.1/" + entry = MockConfigEntry( + domain=DOMAIN, title=base_url, data={CONF_CONTROLLER: "http://127.0.0.1/"}, + ) + flow = VeraFlowHandler() + flow.hass = hass + options_flow = flow.async_get_options_flow(entry) + + result = await options_flow.async_step_init() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await options_flow.async_step_init( + {CONF_LIGHTS: "1,2;3 4 5_6bb7", CONF_EXCLUDE: "8,9;10 11 12_13bb14"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result.get("data") == { + CONF_LIGHTS: [1, 2, 3, 4, 5, 6, 7], + CONF_EXCLUDE: [8, 9, 10, 11, 12, 13, 14], + } From c233a253f89462afe4714bc6d383ebd64a7c8f81 Mon Sep 17 00:00:00 2001 From: Robbie Van Gorkom Date: Thu, 9 Jan 2020 04:41:27 -0800 Subject: [PATCH 13/21] Better handling of options. PR feedback changes. --- homeassistant/components/vera/__init__.py | 10 +++-- homeassistant/components/vera/config_flow.py | 40 +++++++++++++++----- homeassistant/components/vera/strings.json | 2 +- tests/components/vera/test_config_flow.py | 15 +++++--- 4 files changed, 47 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 99f61f26e15e30..7a0129d2adbe23 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -72,10 +72,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Do setup of vera.""" config = config_entry.data - # Get Vera specific configuration. + # Copy the configuration.yml options into the options. base_url = config.get(CONF_CONTROLLER) - light_ids = config_entry.options.get(CONF_LIGHTS, []) - exclude_ids = config_entry.options.get(CONF_EXCLUDE, []) + light_ids = config_entry.options.setdefault( + CONF_LIGHTS, config.get(CONF_LIGHTS, []) + ) + exclude_ids = config_entry.options.setdefault( + CONF_EXCLUDE, config.get(CONF_EXCLUDE, []) + ) # Initialize the Vera controller. controller = veraApi.VeraController(base_url) diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py index 3ac963234d4b97..f00a85862c9edb 100644 --- a/homeassistant/components/vera/config_flow.py +++ b/homeassistant/components/vera/config_flow.py @@ -15,29 +15,51 @@ LIST_REGEX = re.compile("[^0-9]+") -def parse_int_list(data: str) -> List[int]: - """Parse a string into a list of ints.""" +def str_to_int_list(data: str) -> List[int]: + """Convert a string to an int list.""" return [int(s) for s in LIST_REGEX.split(data) if len(s) > 0] +def int_list_to_str(data: List[int]) -> str: + """Convert an int list to a string.""" + return " ".join([str(i) for i in data]) + + class OptionsFlowHandler(config_entries.OptionsFlow): """Options for the component.""" + def __init__(self, config_entry: config_entries.ConfigEntry): + """Init object.""" + self.config_entry = config_entry + async def async_step_init(self, user_input=None): """Manage the options.""" if user_input is not None: return self.async_create_entry( title="", data={ - CONF_LIGHTS: parse_int_list(user_input.get(CONF_LIGHTS, "")), - CONF_EXCLUDE: parse_int_list(user_input.get(CONF_EXCLUDE, "")), + CONF_LIGHTS: str_to_int_list(user_input.get(CONF_LIGHTS, "")), + CONF_EXCLUDE: str_to_int_list(user_input.get(CONF_EXCLUDE, "")), }, ) return self.async_show_form( step_id="init", data_schema=vol.Schema( - {vol.Optional(CONF_LIGHTS): str, vol.Optional(CONF_EXCLUDE): str} + { + vol.Optional( + CONF_LIGHTS, + default=int_list_to_str( + self.config_entry.options.get(CONF_LIGHTS, []) + ), + ): str, + vol.Optional( + CONF_EXCLUDE, + default=int_list_to_str( + self.config_entry.options.get(CONF_EXCLUDE, []) + ), + ): str, + } ), ) @@ -49,7 +71,7 @@ class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @callback def async_get_options_flow(config_entry) -> OptionsFlowHandler: """Get the options flow.""" - return OptionsFlowHandler() + return OptionsFlowHandler(config_entry) async def async_step_user(self, user_input: dict = None): """Handle user initiated flow.""" @@ -77,14 +99,12 @@ async def async_step_finish(self, config: dict): controller = pv.VeraController(base_url) try: - await self.hass.async_add_job(controller.refresh_data) + await self.hass.async_add_executor_job(controller.refresh_data) except RequestException: return self.async_abort( reason="cannot_connect", description_placeholders={"base_url": base_url} ) - await self.async_set_unique_id( - controller.serial_number, raise_on_progress=False - ) + await self.async_set_unique_id(controller.serial_number) return self.async_create_entry(title=base_url, data=config) diff --git a/homeassistant/components/vera/strings.json b/homeassistant/components/vera/strings.json index f7ef36080e7cf1..9d0ef640b32128 100644 --- a/homeassistant/components/vera/strings.json +++ b/homeassistant/components/vera/strings.json @@ -19,7 +19,7 @@ "step": { "init": { "title": "Vera controller options", - "description": "See the vera documentation for details on optional parameters: https://www.home-assistant.io/integrations/vera/", + "description": "See the vera documentation for details on optional parameters: https://www.home-assistant.io/integrations/vera/. Note: Any changes here will need a restart to the home assistant server.", "data": { "lights": "Vera switch device ids to treat as lights in Home Assistant.", "exclude": "Vera device ids to exclude from Home Assistant." diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index a4bc9a651d900b..00e19d6470de3c 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -119,16 +119,19 @@ async def test_options(hass): entry = MockConfigEntry( domain=DOMAIN, title=base_url, data={CONF_CONTROLLER: "http://127.0.0.1/"}, ) - flow = VeraFlowHandler() - flow.hass = hass - options_flow = flow.async_get_options_flow(entry) + entry.options[CONF_LIGHTS] = [1, 2, 3] + entry.add_to_hass(hass) - result = await options_flow.async_step_init() + result = await hass.config_entries.options.flow.async_init( + entry.entry_id, context={"source": "test"}, data=None + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "init" - result = await options_flow.async_step_init( - {CONF_LIGHTS: "1,2;3 4 5_6bb7", CONF_EXCLUDE: "8,9;10 11 12_13bb14"} + result = await hass.config_entries.options.flow.async_init( + entry.entry_id, + context={"source": "test"}, + data={CONF_LIGHTS: "1,2;3 4 5_6bb7", CONF_EXCLUDE: "8,9;10 11 12_13bb14"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result.get("data") == { From 6bb7f105318ebbd8b3aac1078e7ff9ffcfe224d5 Mon Sep 17 00:00:00 2001 From: Robbie Van Gorkom Date: Thu, 9 Jan 2020 07:03:56 -0800 Subject: [PATCH 14/21] Using config registry to update config options. --- homeassistant/components/vera/__init__.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 7a0129d2adbe23..d4bb9cd90e3646 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -1,6 +1,7 @@ """Support for Vera devices.""" import asyncio from collections import defaultdict +from copy import deepcopy import logging import pyvera as veraApi @@ -73,13 +74,18 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): config = config_entry.data # Copy the configuration.yml options into the options. + if CONF_LIGHTS in config or CONF_EXCLUDE in config: + options = deepcopy(config_entry.options) + options.setdefault(CONF_LIGHTS, config.get(CONF_LIGHTS, [])) + options.setdefault(CONF_EXCLUDE, config.get(CONF_EXCLUDE, [])) + + hass.config_entries.async_update_entry( + entry=config_entry, data=config_entry.data, options=options + ) + base_url = config.get(CONF_CONTROLLER) - light_ids = config_entry.options.setdefault( - CONF_LIGHTS, config.get(CONF_LIGHTS, []) - ) - exclude_ids = config_entry.options.setdefault( - CONF_EXCLUDE, config.get(CONF_EXCLUDE, []) - ) + light_ids = config_entry.options.get(CONF_LIGHTS, []) + exclude_ids = config_entry.options.get(CONF_EXCLUDE, []) # Initialize the Vera controller. controller = veraApi.VeraController(base_url) From 5797c6c1d4e5cc28326c31f443e5431015fb0306 Mon Sep 17 00:00:00 2001 From: Robbie Van Gorkom Date: Sat, 11 Jan 2020 11:00:01 -0800 Subject: [PATCH 15/21] Better managing config from file or config from UI Disabling config through UI if config is provided from a file. More tests to account for these adjustments. --- homeassistant/components/vera/__init__.py | 65 ++++++++++----- homeassistant/components/vera/config_flow.py | 68 +++++++++------- homeassistant/components/vera/strings.json | 11 ++- tests/components/vera/common.py | 35 ++++++-- tests/components/vera/test_climate.py | 2 +- tests/components/vera/test_config_flow.py | 8 +- tests/components/vera/test_init.py | 84 ++++++++++++++++++-- tests/components/vera/test_sensor.py | 6 +- 8 files changed, 214 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index d4bb9cd90e3646..127f25654a687b 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -1,7 +1,6 @@ """Support for Vera devices.""" import asyncio from collections import defaultdict -from copy import deepcopy import logging import pyvera as veraApi @@ -17,6 +16,7 @@ ATTR_TRIPPED, CONF_EXCLUDE, CONF_LIGHTS, + CONF_SOURCE, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import HomeAssistant @@ -26,6 +26,7 @@ from homeassistant.util.dt import utc_from_timestamp from .common import ControllerData, get_configured_platforms +from .config_flow import new_options from .const import ( ATTR_CURRENT_ENERGY_KWH, ATTR_CURRENT_POWER_W, @@ -56,34 +57,62 @@ async def async_setup(hass: HomeAssistant, base_config: dict) -> bool: """Set up for Vera controllers.""" config = base_config.get(DOMAIN) - if config is None: + entries = hass.config_entries.async_entries(DOMAIN) + import_entries = [ + entry + for entry in entries + if entry.data.get(CONF_SOURCE) == config_entries.SOURCE_IMPORT + ] + + if not config: + for entry in import_entries: + _LOGGER.debug( + "Removing existing import config for %s", + entry.data.get(CONF_CONTROLLER), + ) + await hass.config_entries.async_remove(entry.entry_id) + return True - _LOGGER.debug("Creating new config for %s", config.get(CONF_CONTROLLER)) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config, + if import_entries: + _LOGGER.debug( + "Updating existing import config for %s", config.get(CONF_CONTROLLER) + ) + hass.config_entries.async_update_entry( + entry=import_entries[0], + data={ + CONF_CONTROLLER: config.get(CONF_CONTROLLER), + CONF_SOURCE: config_entries.SOURCE_IMPORT, + }, + options=new_options( + config.get(CONF_LIGHTS, []), config.get(CONF_EXCLUDE, []) + ), + ) + else: + _LOGGER.debug("Creating new import config for %s", config.get(CONF_CONTROLLER)) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config, + ) ) - ) return True -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Do setup of vera.""" - config = config_entry.data - - # Copy the configuration.yml options into the options. - if CONF_LIGHTS in config or CONF_EXCLUDE in config: - options = deepcopy(config_entry.options) - options.setdefault(CONF_LIGHTS, config.get(CONF_LIGHTS, [])) - options.setdefault(CONF_EXCLUDE, config.get(CONF_EXCLUDE, [])) - + # Use options entered during initial config flow or provided from configuration.yml + if config_entry.data.get(CONF_LIGHTS) or config_entry.data.get(CONF_EXCLUDE): hass.config_entries.async_update_entry( - entry=config_entry, data=config_entry.data, options=options + entry=config_entry, + data=config_entry.data, + options=new_options( + config_entry.data.get(CONF_LIGHTS, []), + config_entry.data.get(CONF_EXCLUDE, []), + ), ) - base_url = config.get(CONF_CONTROLLER) + base_url = config_entry.data.get(CONF_CONTROLLER) light_ids = config_entry.options.get(CONF_LIGHTS, []) exclude_ids = config_entry.options.get(CONF_EXCLUDE, []) diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py index f00a85862c9edb..f929eb4e3861f5 100644 --- a/homeassistant/components/vera/config_flow.py +++ b/homeassistant/components/vera/config_flow.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS +from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE from homeassistant.core import callback from .const import CONF_CONTROLLER, DOMAIN @@ -25,6 +25,32 @@ def int_list_to_str(data: List[int]) -> str: return " ".join([str(i) for i in data]) +def new_options(lights: List[int], exclude: List[int]) -> dict: + """Create a standard options object.""" + return {CONF_LIGHTS: lights, CONF_EXCLUDE: exclude} + + +def options_schema(options: dict = None) -> dict: + """Return options schema.""" + options = options or {} + return { + vol.Optional( + CONF_LIGHTS, default=int_list_to_str(options.get(CONF_LIGHTS, [])), + ): str, + vol.Optional( + CONF_EXCLUDE, default=int_list_to_str(options.get(CONF_EXCLUDE, [])), + ): str, + } + + +def options_data(user_input: dict) -> dict: + """Return options dict.""" + return new_options( + str_to_int_list(user_input.get(CONF_LIGHTS, "")), + str_to_int_list(user_input.get(CONF_EXCLUDE, "")), + ) + + class OptionsFlowHandler(config_entries.OptionsFlow): """Options for the component.""" @@ -34,33 +60,15 @@ def __init__(self, config_entry: config_entries.ConfigEntry): async def async_step_init(self, user_input=None): """Manage the options.""" + if self.config_entry.data.get(CONF_SOURCE) == config_entries.SOURCE_IMPORT: + return self.async_show_form(step_id="from_config") + if user_input is not None: - return self.async_create_entry( - title="", - data={ - CONF_LIGHTS: str_to_int_list(user_input.get(CONF_LIGHTS, "")), - CONF_EXCLUDE: str_to_int_list(user_input.get(CONF_EXCLUDE, "")), - }, - ) + return self.async_create_entry(title="", data=options_data(user_input),) return self.async_show_form( step_id="init", - data_schema=vol.Schema( - { - vol.Optional( - CONF_LIGHTS, - default=int_list_to_str( - self.config_entry.options.get(CONF_LIGHTS, []) - ), - ): str, - vol.Optional( - CONF_EXCLUDE, - default=int_list_to_str( - self.config_entry.options.get(CONF_EXCLUDE, []) - ), - ): str, - } - ), + data_schema=vol.Schema(options_schema(self.config_entry.options)), ) @@ -79,11 +87,15 @@ async def async_step_user(self, user_input: dict = None): return self.async_abort(reason="already_setup") if user_input is not None: - return await self.async_step_finish(user_input) + return await self.async_step_finish( + {**user_input, **{CONF_SOURCE: config_entries.SOURCE_USER}} + ) return self.async_show_form( step_id="user", - data_schema=vol.Schema({vol.Required(CONF_CONTROLLER): str}), + data_schema=vol.Schema( + {**{vol.Required(CONF_CONTROLLER): str}, **options_schema()} + ), ) async def async_step_import(self, config: dict): @@ -91,7 +103,9 @@ async def async_step_import(self, config: dict): if self.hass.config_entries.async_entries(DOMAIN): return self.async_abort(reason="already_setup") - return await self.async_step_finish(config) + return await self.async_step_finish( + {**config, **{CONF_SOURCE: config_entries.SOURCE_IMPORT}} + ) async def async_step_finish(self, config: dict): """Validate and create config entry.""" diff --git a/homeassistant/components/vera/strings.json b/homeassistant/components/vera/strings.json index 9d0ef640b32128..86e600d5262dd7 100644 --- a/homeassistant/components/vera/strings.json +++ b/homeassistant/components/vera/strings.json @@ -10,12 +10,17 @@ "title": "Setup Vera controller", "description": "Provide a Vera controller url below. It should look like this: http://192.168.1.161:3480.", "data": { - "vera_controller_url": "Controller URL" + "vera_controller_url": "Controller URL", + "lights": "Vera switch device ids to treat as lights in Home Assistant.", + "exclude": "Vera device ids to exclude from Home Assistant." } } } }, "options": { + "abort": { + "file_based_config": "Configuration for this Vera controller is handled in configuration.yml. Please make changes there." + }, "step": { "init": { "title": "Vera controller options", @@ -24,6 +29,10 @@ "lights": "Vera switch device ids to treat as lights in Home Assistant.", "exclude": "Vera device ids to exclude from Home Assistant." } + }, + "from_config": { + "title": "UI config not supported", + "description": "Controller was configured using configuration.yml. Config changes should be made in the config file." } } } diff --git a/tests/components/vera/common.py b/tests/components/vera/common.py index 74372e781a4f09..3dbcade1322a93 100644 --- a/tests/components/vera/common.py +++ b/tests/components/vera/common.py @@ -5,6 +5,7 @@ from mock import MagicMock import pyvera as pv +from homeassistant import config_entries from homeassistant.components.vera.const import CONF_CONTROLLER, DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -21,6 +22,8 @@ "ControllerConfig", ( ("config", Dict), + ("options", Dict), + ("config_from_file", bool), ("serial_number", str), ("devices", Tuple[pv.VeraDevice, ...]), ("scenes", Tuple[pv.VeraScene, ...]), @@ -30,7 +33,9 @@ def new_simple_controller_config( - base_url="http://127.0.0.1:123", + config: dict = None, + options: dict = None, + config_from_file=False, serial_number="1111", devices: Tuple[pv.VeraDevice, ...] = (), scenes: Tuple[pv.VeraScene, ...] = (), @@ -38,7 +43,9 @@ def new_simple_controller_config( ) -> ControllerConfig: """Create simple contorller config.""" return ControllerConfig( - config={CONF_CONTROLLER: base_url}, + config=config or {CONF_CONTROLLER: "http://127.0.0.1:123"}, + options=options, + config_from_file=config_from_file, serial_number=serial_number, devices=devices, scenes=scenes, @@ -57,12 +64,13 @@ async def configure_component( self, hass: HomeAssistant, controller_config: ControllerConfig ) -> ComponentData: """Configure the component with specific mock data.""" - hass_config = { - DOMAIN: controller_config.config, + component_config = { + **(controller_config.config or {}), + **(controller_config.options or {}), } controller = MagicMock(spec=pv.VeraController) # type: pv.VeraController - controller.base_url = controller_config.config.get(CONF_CONTROLLER) + controller.base_url = component_config.get(CONF_CONTROLLER) controller.register = MagicMock() controller.start = MagicMock() controller.stop = MagicMock() @@ -79,14 +87,29 @@ async def configure_component( controller.get_scenes.reset_mock() if controller_config.setup_callback: - controller_config.setup_callback(controller, hass_config) + controller_config.setup_callback(controller) self.vera_controller_class_mock.return_value = controller + hass_config = {} + + # Setup component through config file import. + if controller_config.config_from_file: + hass_config[DOMAIN] = component_config + # Setup Home Assistant. assert await async_setup_component(hass, DOMAIN, hass_config) await hass.async_block_till_done() + # Setup component through config flow. + if not controller_config.config_from_file: + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=component_config, + ) + await hass.async_block_till_done() + update_callback = ( controller.register.call_args_list[0][0][1] if controller.register.call_args_list diff --git a/tests/components/vera/test_climate.py b/tests/components/vera/test_climate.py index 66aa9a2f8a9085..9e5fa983ed053f 100644 --- a/tests/components/vera/test_climate.py +++ b/tests/components/vera/test_climate.py @@ -133,7 +133,7 @@ async def test_climate_f( vera_device.get_current_goal_temperature.return_value = 72 entity_id = "climate.dev1_1" - def setup_callback(controller: pv.VeraController, hass_config: dict) -> None: + def setup_callback(controller: pv.VeraController) -> None: controller.temperature_units = "F" component_data = await vera_component_factory.configure_component( diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index 00e19d6470de3c..1cbc155e4ccdb7 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -7,7 +7,7 @@ from homeassistant import config_entries, data_entry_flow from homeassistant.components.vera import CONF_CONTROLLER, DOMAIN from homeassistant.components.vera.config_flow import VeraFlowHandler -from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS +from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -41,6 +41,7 @@ async def test_aync_step_user_success(hass: HomeAssistant) -> None: assert result.get("title") == "http://127.0.0.1:123" assert result.get("data") == { CONF_CONTROLLER: "http://127.0.0.1:123", + CONF_SOURCE: config_entries.SOURCE_USER, } assert result.get("result").unique_id == controller.serial_number @@ -75,7 +76,10 @@ async def test_aync_step_import_success(hass: HomeAssistant) -> None: assert result.get("type") == RESULT_TYPE_CREATE_ENTRY assert result.get("title") == "http://127.0.0.1:123" - assert result.get("data") == {CONF_CONTROLLER: "http://127.0.0.1:123"} + assert result.get("data") == { + CONF_CONTROLLER: "http://127.0.0.1:123", + CONF_SOURCE: config_entries.SOURCE_IMPORT, + } assert result.get("result").unique_id == controller.serial_number diff --git a/tests/components/vera/test_init.py b/tests/components/vera/test_init.py index fc84e5209dccf2..bccdf96c41c7f6 100644 --- a/tests/components/vera/test_init.py +++ b/tests/components/vera/test_init.py @@ -1,16 +1,18 @@ """Vera tests.""" -from unittest.mock import MagicMock - +from asynctest import CoroutineMock, MagicMock import pyvera as pv from requests.exceptions import RequestException +from homeassistant import config_entries from homeassistant.components.vera import ( CONF_CONTROLLER, DOMAIN, + async_setup, async_setup_entry, async_unload_entry, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config @@ -30,7 +32,8 @@ async def test_init( await vera_component_factory.configure_component( hass=hass, controller_config=new_simple_controller_config( - base_url="http://127.0.0.1:111", + config={CONF_CONTROLLER: "http://127.0.0.1:111"}, + config_from_file=False, serial_number="first_serial", devices=(vera_device1,), ), @@ -42,6 +45,73 @@ async def test_init( assert entry1 +async def test_init_from_file( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor + vera_device1.device_id = 1 + vera_device1.vera_device_id = 1 + vera_device1.name = "first_dev" + vera_device1.is_tripped = False + entity1_id = "binary_sensor.first_dev_1" + + await vera_component_factory.configure_component( + hass=hass, + controller_config=new_simple_controller_config( + config={CONF_CONTROLLER: "http://127.0.0.1:111"}, + config_from_file=True, + serial_number="first_serial", + devices=(vera_device1,), + ), + ) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + entry1 = entity_registry.async_get(entity1_id) + assert entry1 + + +async def test_async_setup_remove_configs(hass: HomeAssistant) -> None: + """Test function.""" + entry1 = MagicMock(spec=ConfigEntry) + entry1.entry_id = "id1" + entry1.domain = DOMAIN + entry1.data = {CONF_CONTROLLER: "url1", CONF_SOURCE: config_entries.SOURCE_IMPORT} + + entry2 = MagicMock(spec=ConfigEntry) + entry2.entry_id = "id2" + entry2.domain = DOMAIN + entry2.data = {CONF_CONTROLLER: "url2", CONF_SOURCE: config_entries.SOURCE_USER} + + hass.config_entries.async_entries = MagicMock(return_value=[entry1, entry2]) + hass.config_entries.async_remove = remove_mock = CoroutineMock() + + await async_setup(hass, {}) + remove_mock.assert_called_with("id1") + assert remove_mock.call_count == 1 + + +async def test_async_setup_update_configs(hass: HomeAssistant) -> None: + """Test function.""" + entry1 = MagicMock(spec=ConfigEntry) + entry1.entry_id = "id1" + entry1.domain = DOMAIN + entry1.data = {CONF_CONTROLLER: "url1", CONF_SOURCE: config_entries.SOURCE_IMPORT} + + hass.config_entries.async_entries = MagicMock(return_value=[entry1]) + hass.config_entries.async_update_entry = update_mock = MagicMock() + + await async_setup( + hass, + {DOMAIN: {CONF_CONTROLLER: "url2", CONF_LIGHTS: [1, 2], CONF_EXCLUDE: [3, 4]}}, + ) + update_mock.assert_called_with( + entry=entry1, + data={CONF_CONTROLLER: "url2", CONF_SOURCE: config_entries.SOURCE_IMPORT}, + options={CONF_LIGHTS: [1, 2], CONF_EXCLUDE: [3, 4]}, + ) + + async def test_unload( hass: HomeAssistant, vera_component_factory: ComponentFactory ) -> None: @@ -56,10 +126,10 @@ async def test_unload( hass=hass, controller_config=new_simple_controller_config() ) - config_entries = hass.config_entries.async_entries(DOMAIN) - assert config_entries + entries = hass.config_entries.async_entries(DOMAIN) + assert entries - for config_entry in config_entries: + for config_entry in entries: await async_unload_entry(hass, config_entry) @@ -68,7 +138,7 @@ async def test_async_setup_entry_error( ) -> None: """Test function.""" - def setup_callback(controller: pv.VeraController, config: dict) -> None: + def setup_callback(controller: pv.VeraController) -> None: controller.get_devices.side_effect = RequestException() controller.get_scenes.side_effect = RequestException() diff --git a/tests/components/vera/test_sensor.py b/tests/components/vera/test_sensor.py index cd2e0613970206..c915c5ead0fd17 100644 --- a/tests/components/vera/test_sensor.py +++ b/tests/components/vera/test_sensor.py @@ -50,7 +50,7 @@ async def test_temperature_sensor_f( ) -> None: """Test function.""" - def setup_callback(controller: pv.VeraController, hass_config: dict) -> None: + def setup_callback(controller: pv.VeraController) -> None: controller.temperature_units = "F" await run_sensor_test( @@ -137,7 +137,7 @@ async def test_trippable_sensor( ) -> None: """Test function.""" - def setup_callback(controller: pv.VeraController, hass_config: dict) -> None: + def setup_callback(controller: pv.VeraController) -> None: controller.get_devices()[0].is_trippable = True await run_sensor_test( @@ -155,7 +155,7 @@ async def test_unknown_sensor( ) -> None: """Test function.""" - def setup_callback(controller: pv.VeraController, hass_config: dict) -> None: + def setup_callback(controller: pv.VeraController) -> None: controller.get_devices()[0].is_trippable = False await run_sensor_test( From 62a5667214efd810009a176f32e6916c8bb5b3c5 Mon Sep 17 00:00:00 2001 From: Robbie Van Gorkom Date: Sat, 11 Jan 2020 12:23:51 -0800 Subject: [PATCH 16/21] Address PR feedback. --- homeassistant/components/vera/__init__.py | 13 +--- homeassistant/components/vera/config_flow.py | 10 ++- tests/components/vera/test_init.py | 66 +++++++++----------- 3 files changed, 41 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 127f25654a687b..3ba88412f3446c 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -57,6 +57,9 @@ async def async_setup(hass: HomeAssistant, base_config: dict) -> bool: """Set up for Vera controllers.""" config = base_config.get(DOMAIN) + if not config: + return True + entries = hass.config_entries.async_entries(DOMAIN) import_entries = [ entry @@ -64,16 +67,6 @@ async def async_setup(hass: HomeAssistant, base_config: dict) -> bool: if entry.data.get(CONF_SOURCE) == config_entries.SOURCE_IMPORT ] - if not config: - for entry in import_entries: - _LOGGER.debug( - "Removing existing import config for %s", - entry.data.get(CONF_CONTROLLER), - ) - await hass.config_entries.async_remove(entry.entry_id) - - return True - if import_entries: _LOGGER.debug( "Updating existing import config for %s", config.get(CONF_CONTROLLER) diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py index f929eb4e3861f5..264f05ebb15b0d 100644 --- a/homeassistant/components/vera/config_flow.py +++ b/homeassistant/components/vera/config_flow.py @@ -58,10 +58,18 @@ def __init__(self, config_entry: config_entries.ConfigEntry): """Init object.""" self.config_entry = config_entry + async def async_step_from_config(self): + """Manage empty options. + + This should do nothing and not allow any action. This occurs when the user uses the UI + to attempt to change options for a config that was provided in configuration.yml. + """ + return self.async_show_form(step_id="from_config") + async def async_step_init(self, user_input=None): """Manage the options.""" if self.config_entry.data.get(CONF_SOURCE) == config_entries.SOURCE_IMPORT: - return self.async_show_form(step_id="from_config") + return await self.async_step_from_config() if user_input is not None: return self.async_create_entry(title="", data=options_data(user_input),) diff --git a/tests/components/vera/test_init.py b/tests/components/vera/test_init.py index bccdf96c41c7f6..7659af39e95201 100644 --- a/tests/components/vera/test_init.py +++ b/tests/components/vera/test_init.py @@ -1,5 +1,5 @@ """Vera tests.""" -from asynctest import CoroutineMock, MagicMock +from asynctest import MagicMock import pyvera as pv from requests.exceptions import RequestException @@ -7,7 +7,6 @@ from homeassistant.components.vera import ( CONF_CONTROLLER, DOMAIN, - async_setup, async_setup_entry, async_unload_entry, ) @@ -17,6 +16,8 @@ from .common import ComponentFactory, new_simple_controller_config +from tests.common import MockConfigEntry + async def test_init( hass: HomeAssistant, vera_component_factory: ComponentFactory @@ -71,46 +72,37 @@ async def test_init_from_file( assert entry1 -async def test_async_setup_remove_configs(hass: HomeAssistant) -> None: - """Test function.""" - entry1 = MagicMock(spec=ConfigEntry) - entry1.entry_id = "id1" - entry1.domain = DOMAIN - entry1.data = {CONF_CONTROLLER: "url1", CONF_SOURCE: config_entries.SOURCE_IMPORT} - - entry2 = MagicMock(spec=ConfigEntry) - entry2.entry_id = "id2" - entry2.domain = DOMAIN - entry2.data = {CONF_CONTROLLER: "url2", CONF_SOURCE: config_entries.SOURCE_USER} - - hass.config_entries.async_entries = MagicMock(return_value=[entry1, entry2]) - hass.config_entries.async_remove = remove_mock = CoroutineMock() - - await async_setup(hass, {}) - remove_mock.assert_called_with("id1") - assert remove_mock.call_count == 1 - - -async def test_async_setup_update_configs(hass: HomeAssistant) -> None: +async def test_async_setup_update_configs( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: """Test function.""" - entry1 = MagicMock(spec=ConfigEntry) - entry1.entry_id = "id1" - entry1.domain = DOMAIN - entry1.data = {CONF_CONTROLLER: "url1", CONF_SOURCE: config_entries.SOURCE_IMPORT} - - hass.config_entries.async_entries = MagicMock(return_value=[entry1]) - hass.config_entries.async_update_entry = update_mock = MagicMock() + entry1 = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_CONTROLLER: "http://url1:123", + CONF_SOURCE: config_entries.SOURCE_IMPORT, + }, + ) + entry1.add_to_hass(hass) - await async_setup( + await vera_component_factory.configure_component( hass, - {DOMAIN: {CONF_CONTROLLER: "url2", CONF_LIGHTS: [1, 2], CONF_EXCLUDE: [3, 4]}}, - ) - update_mock.assert_called_with( - entry=entry1, - data={CONF_CONTROLLER: "url2", CONF_SOURCE: config_entries.SOURCE_IMPORT}, - options={CONF_LIGHTS: [1, 2], CONF_EXCLUDE: [3, 4]}, + new_simple_controller_config( + config={CONF_CONTROLLER: "http://url2:123"}, + options={CONF_LIGHTS: [1, 2], CONF_EXCLUDE: [3, 4]}, + config_from_file=True, + ), ) + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + entry = entries[0] + assert entry.data == { + CONF_CONTROLLER: "http://url2:123", + CONF_SOURCE: config_entries.SOURCE_IMPORT, + } + assert entry.options == {CONF_LIGHTS: [1, 2], CONF_EXCLUDE: [3, 4]} + async def test_unload( hass: HomeAssistant, vera_component_factory: ComponentFactory From aa952092cfaae900ebc4b879d7b9c81cc874f2b2 Mon Sep 17 00:00:00 2001 From: Robbie Van Gorkom Date: Sat, 11 Jan 2020 13:39:38 -0800 Subject: [PATCH 17/21] Fixing test, merging with master. --- tests/components/vera/test_config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index 1cbc155e4ccdb7..b38ef1d422b4b5 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -126,13 +126,13 @@ async def test_options(hass): entry.options[CONF_LIGHTS] = [1, 2, 3] entry.add_to_hass(hass) - result = await hass.config_entries.options.flow.async_init( + result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "init" - result = await hass.config_entries.options.flow.async_init( + result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data={CONF_LIGHTS: "1,2;3 4 5_6bb7", CONF_EXCLUDE: "8,9;10 11 12_13bb14"}, From 91140f619cd9e0b597b2c4b8ca4b4a95ddc403da Mon Sep 17 00:00:00 2001 From: Robbie Van Gorkom Date: Sat, 11 Jan 2020 17:39:34 -0800 Subject: [PATCH 18/21] Disabling all Vera UI for configs managed by configuration.yml. Adding more tests. --- homeassistant/components/vera/config_flow.py | 33 ++++++------ homeassistant/components/vera/strings.json | 4 -- tests/components/vera/test_config_flow.py | 57 ++++++++++++++++++-- 3 files changed, 70 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py index 264f05ebb15b0d..7341479d7cdc22 100644 --- a/homeassistant/components/vera/config_flow.py +++ b/homeassistant/components/vera/config_flow.py @@ -6,7 +6,7 @@ from requests.exceptions import RequestException import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE from homeassistant.core import callback @@ -15,17 +15,20 @@ LIST_REGEX = re.compile("[^0-9]+") -def str_to_int_list(data: str) -> List[int]: +def str_to_int_list(data: str) -> List[str]: """Convert a string to an int list.""" - return [int(s) for s in LIST_REGEX.split(data) if len(s) > 0] + if isinstance(str, list): + return data + return [s for s in LIST_REGEX.split(data) if len(s) > 0] -def int_list_to_str(data: List[int]) -> str: + +def int_list_to_str(data: List[str]) -> str: """Convert an int list to a string.""" return " ".join([str(i) for i in data]) -def new_options(lights: List[int], exclude: List[int]) -> dict: +def new_options(lights: List[str], exclude: List[str]) -> dict: """Create a standard options object.""" return {CONF_LIGHTS: lights, CONF_EXCLUDE: exclude} @@ -58,19 +61,8 @@ def __init__(self, config_entry: config_entries.ConfigEntry): """Init object.""" self.config_entry = config_entry - async def async_step_from_config(self): - """Manage empty options. - - This should do nothing and not allow any action. This occurs when the user uses the UI - to attempt to change options for a config that was provided in configuration.yml. - """ - return self.async_show_form(step_id="from_config") - async def async_step_init(self, user_input=None): """Manage the options.""" - if self.config_entry.data.get(CONF_SOURCE) == config_entries.SOURCE_IMPORT: - return await self.async_step_from_config() - if user_input is not None: return self.async_create_entry(title="", data=options_data(user_input),) @@ -87,6 +79,9 @@ class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @callback def async_get_options_flow(config_entry) -> OptionsFlowHandler: """Get the options flow.""" + if config_entry.data.get(CONF_SOURCE) == config_entries.SOURCE_IMPORT: + raise data_entry_flow.UnknownHandler + return OptionsFlowHandler(config_entry) async def async_step_user(self, user_input: dict = None): @@ -96,7 +91,11 @@ async def async_step_user(self, user_input: dict = None): if user_input is not None: return await self.async_step_finish( - {**user_input, **{CONF_SOURCE: config_entries.SOURCE_USER}} + { + **user_input, + **options_data(user_input), + **{CONF_SOURCE: config_entries.SOURCE_USER}, + } ) return self.async_show_form( diff --git a/homeassistant/components/vera/strings.json b/homeassistant/components/vera/strings.json index 86e600d5262dd7..8c9db462f1c22e 100644 --- a/homeassistant/components/vera/strings.json +++ b/homeassistant/components/vera/strings.json @@ -29,10 +29,6 @@ "lights": "Vera switch device ids to treat as lights in Home Assistant.", "exclude": "Vera device ids to exclude from Home Assistant." } - }, - "from_config": { - "title": "UI config not supported", - "description": "Controller was configured using configuration.yml. Config changes should be made in the config file." } } } diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index b38ef1d422b4b5..199a12f87b157e 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock from mock import patch +import pytest from requests.exceptions import RequestException from homeassistant import config_entries, data_entry_flow @@ -35,16 +36,25 @@ async def test_aync_step_user_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, - data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, + data={ + CONF_CONTROLLER: "http://127.0.0.1:123/", + CONF_LIGHTS: "12 13", + CONF_EXCLUDE: "14 15", + }, ) assert result.get("type") == RESULT_TYPE_CREATE_ENTRY assert result.get("title") == "http://127.0.0.1:123" assert result.get("data") == { CONF_CONTROLLER: "http://127.0.0.1:123", CONF_SOURCE: config_entries.SOURCE_USER, + CONF_LIGHTS: ["12", "13"], + CONF_EXCLUDE: ["14", "15"], } assert result.get("result").unique_id == controller.serial_number + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + async def test_async_step_user_already_setup(hass: HomeAssistant) -> None: """Test function.""" @@ -117,6 +127,47 @@ async def test_async_step_finish_error(hass: HomeAssistant) -> None: } +async def test_options_not_available_for_file_config(hass: HomeAssistant) -> None: + """Test function.""" + with patch("pyvera.VeraController") as vera_controller_class_mock: + controller = MagicMock() + controller.refresh_data = MagicMock() + controller.serial_number = "serial_number_1" + vera_controller_class_mock.return_value = controller + + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, + ) + entries = hass.config_entries.async_entries(DOMAIN) + + with pytest.raises(data_entry_flow.UnknownHandler): + await hass.config_entries.options.async_init( + entries[0].entry_id, context={}, data={} + ) + + +async def test_options_available_ui_config(hass: HomeAssistant) -> None: + """Test function.""" + with patch("pyvera.VeraController") as vera_controller_class_mock: + controller = MagicMock() + controller.refresh_data = MagicMock() + controller.serial_number = "serial_number_1" + vera_controller_class_mock.return_value = controller + + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, + ) + entries = hass.config_entries.async_entries(DOMAIN) + + await hass.config_entries.options.async_init( + entries[0].entry_id, context={}, data={} + ) + + async def test_options(hass): """Test updating options.""" base_url = "http://127.0.0.1/" @@ -139,6 +190,6 @@ async def test_options(hass): ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result.get("data") == { - CONF_LIGHTS: [1, 2, 3, 4, 5, 6, 7], - CONF_EXCLUDE: [8, 9, 10, 11, 12, 13, 14], + CONF_LIGHTS: ["1", "2", "3", "4", "5", "6", "7"], + CONF_EXCLUDE: ["8", "9", "10", "11", "12", "13", "14"], } From bf8f5f99316a5f8ee3dcfecbae67c9769d9ed40e Mon Sep 17 00:00:00 2001 From: Robbie Van Gorkom Date: Tue, 14 Jan 2020 05:59:40 -0800 Subject: [PATCH 19/21] Updating config based on unique_id. Addressing additional PR feedback. --- homeassistant/components/vera/__init__.py | 32 ++---------- homeassistant/components/vera/config_flow.py | 37 +++++++++++--- tests/components/vera/test_config_flow.py | 52 ++++++++------------ tests/components/vera/test_init.py | 36 -------------- 4 files changed, 55 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 3ba88412f3446c..25ed3824e06b51 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -16,7 +16,6 @@ ATTR_TRIPPED, CONF_EXCLUDE, CONF_LIGHTS, - CONF_SOURCE, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import HomeAssistant @@ -60,34 +59,11 @@ async def async_setup(hass: HomeAssistant, base_config: dict) -> bool: if not config: return True - entries = hass.config_entries.async_entries(DOMAIN) - import_entries = [ - entry - for entry in entries - if entry.data.get(CONF_SOURCE) == config_entries.SOURCE_IMPORT - ] - - if import_entries: - _LOGGER.debug( - "Updating existing import config for %s", config.get(CONF_CONTROLLER) - ) - hass.config_entries.async_update_entry( - entry=import_entries[0], - data={ - CONF_CONTROLLER: config.get(CONF_CONTROLLER), - CONF_SOURCE: config_entries.SOURCE_IMPORT, - }, - options=new_options( - config.get(CONF_LIGHTS, []), config.get(CONF_EXCLUDE, []) - ), - ) - else: - _LOGGER.debug("Creating new import config for %s", config.get(CONF_CONTROLLER)) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config, - ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config, ) + ) return True diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py index 7341479d7cdc22..3f69745c6ee1ea 100644 --- a/homeassistant/components/vera/config_flow.py +++ b/homeassistant/components/vera/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Vera.""" +import logging import re from typing import List @@ -6,13 +7,14 @@ from requests.exceptions import RequestException import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE from homeassistant.core import callback from .const import CONF_CONTROLLER, DOMAIN LIST_REGEX = re.compile("[^0-9]+") +_LOGGER = logging.getLogger(__name__) def str_to_int_list(data: str) -> List[str]: @@ -79,9 +81,6 @@ class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @callback def async_get_options_flow(config_entry) -> OptionsFlowHandler: """Get the options flow.""" - if config_entry.data.get(CONF_SOURCE) == config_entries.SOURCE_IMPORT: - raise data_entry_flow.UnknownHandler - return OptionsFlowHandler(config_entry) async def async_step_user(self, user_input: dict = None): @@ -107,9 +106,6 @@ async def async_step_user(self, user_input: dict = None): async def async_step_import(self, config: dict): """Handle a flow initialized by import.""" - if self.hass.config_entries.async_entries(DOMAIN): - return self.async_abort(reason="already_setup") - return await self.async_step_finish( {**config, **{CONF_SOURCE: config_entries.SOURCE_IMPORT}} ) @@ -119,13 +115,40 @@ async def async_step_finish(self, config: dict): base_url = config[CONF_CONTROLLER] = config.get(CONF_CONTROLLER).rstrip("/") controller = pv.VeraController(base_url) + # Verify the controller is online and get the serial number. try: await self.hass.async_add_executor_job(controller.refresh_data) except RequestException: + _LOGGER.error("Failed to connect to vera controller %s", base_url) return self.async_abort( reason="cannot_connect", description_placeholders={"base_url": base_url} ) + domain_entries = self.hass.config_entries.async_entries(DOMAIN) + existing_config_entry = next( + iter( + [ + entry + for entry in domain_entries + if entry.unique_id == controller.serial_number + ] + ), + None, + ) + + if existing_config_entry: + _LOGGER.debug( + "Updating existing import config for %s", controller.serial_number + ) + # Note: the options get updated in async_setup_entry() + self.hass.config_entries.async_update_entry( + entry=existing_config_entry, data=config, + ) + + # Integration does not support multiple controllers yet. + if domain_entries: + return self.async_abort(reason="already_setup") + await self.async_set_unique_id(controller.serial_number) return self.async_create_entry(title=base_url, data=config) diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index 199a12f87b157e..09888e61c14181 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -2,7 +2,6 @@ from unittest.mock import MagicMock from mock import patch -import pytest from requests.exceptions import RequestException from homeassistant import config_entries, data_entry_flow @@ -58,16 +57,18 @@ async def test_aync_step_user_success(hass: HomeAssistant) -> None: async def test_async_step_user_already_setup(hass: HomeAssistant) -> None: """Test function.""" + entry = MockConfigEntry(domain=DOMAIN, data={}, options={}, unique_id="12345") + entry.add_to_hass(hass) + handler = VeraFlowHandler() handler.context = {} handler.hass = hass - with patch.object(hass.config_entries, "async_entries", return_value=[{}]): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result.get("type") == RESULT_TYPE_ABORT - assert result.get("reason") == "already_setup" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "already_setup" async def test_aync_step_import_success(hass: HomeAssistant) -> None: @@ -95,13 +96,23 @@ async def test_aync_step_import_success(hass: HomeAssistant) -> None: async def test_async_step_import_alredy_setup(hass: HomeAssistant) -> None: """Test function.""" + entry = MockConfigEntry(domain=DOMAIN, data={}, options={}, unique_id="12345") + entry.add_to_hass(hass) + handler = VeraFlowHandler() handler.context = {} handler.hass = hass - with patch.object(hass.config_entries, "async_entries", return_value=[{}]): + with patch("pyvera.VeraController") as vera_controller_class_mock: + controller = MagicMock() + controller.refresh_data = MagicMock() + controller.serial_number = "12345" + vera_controller_class_mock.return_value = controller + result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={} + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_CONTROLLER: "http://localhost:445"}, ) assert result.get("type") == RESULT_TYPE_ABORT assert result.get("reason") == "already_setup" @@ -127,28 +138,7 @@ async def test_async_step_finish_error(hass: HomeAssistant) -> None: } -async def test_options_not_available_for_file_config(hass: HomeAssistant) -> None: - """Test function.""" - with patch("pyvera.VeraController") as vera_controller_class_mock: - controller = MagicMock() - controller.refresh_data = MagicMock() - controller.serial_number = "serial_number_1" - vera_controller_class_mock.return_value = controller - - await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, - ) - entries = hass.config_entries.async_entries(DOMAIN) - - with pytest.raises(data_entry_flow.UnknownHandler): - await hass.config_entries.options.async_init( - entries[0].entry_id, context={}, data={} - ) - - -async def test_options_available_ui_config(hass: HomeAssistant) -> None: +async def test_options_available(hass: HomeAssistant) -> None: """Test function.""" with patch("pyvera.VeraController") as vera_controller_class_mock: controller = MagicMock() diff --git a/tests/components/vera/test_init.py b/tests/components/vera/test_init.py index 7659af39e95201..226108a51e9037 100644 --- a/tests/components/vera/test_init.py +++ b/tests/components/vera/test_init.py @@ -3,7 +3,6 @@ import pyvera as pv from requests.exceptions import RequestException -from homeassistant import config_entries from homeassistant.components.vera import ( CONF_CONTROLLER, DOMAIN, @@ -11,13 +10,10 @@ async_unload_entry, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config -from tests.common import MockConfigEntry - async def test_init( hass: HomeAssistant, vera_component_factory: ComponentFactory @@ -72,38 +68,6 @@ async def test_init_from_file( assert entry1 -async def test_async_setup_update_configs( - hass: HomeAssistant, vera_component_factory: ComponentFactory -) -> None: - """Test function.""" - entry1 = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_CONTROLLER: "http://url1:123", - CONF_SOURCE: config_entries.SOURCE_IMPORT, - }, - ) - entry1.add_to_hass(hass) - - await vera_component_factory.configure_component( - hass, - new_simple_controller_config( - config={CONF_CONTROLLER: "http://url2:123"}, - options={CONF_LIGHTS: [1, 2], CONF_EXCLUDE: [3, 4]}, - config_from_file=True, - ), - ) - - entries = hass.config_entries.async_entries(DOMAIN) - assert entries - entry = entries[0] - assert entry.data == { - CONF_CONTROLLER: "http://url2:123", - CONF_SOURCE: config_entries.SOURCE_IMPORT, - } - assert entry.options == {CONF_LIGHTS: [1, 2], CONF_EXCLUDE: [3, 4]} - - async def test_unload( hass: HomeAssistant, vera_component_factory: ComponentFactory ) -> None: From db1010f0be6bc0e52d75a75140c26de47bbe556b Mon Sep 17 00:00:00 2001 From: Robbie Van Gorkom Date: Sun, 29 Mar 2020 09:24:14 -0700 Subject: [PATCH 20/21] Rebasing off dev. Addressing feedback. --- homeassistant/components/vera/common.py | 14 ++++----- homeassistant/components/vera/config_flow.py | 32 +++----------------- homeassistant/components/vera/sensor.py | 2 +- homeassistant/components/vera/strings.json | 4 +-- tests/components/vera/test_config_flow.py | 12 +++++--- 5 files changed, 20 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/vera/common.py b/homeassistant/components/vera/common.py index d9f3bc493226f6..cdfdff404ec911 100644 --- a/homeassistant/components/vera/common.py +++ b/homeassistant/components/vera/common.py @@ -9,14 +9,12 @@ _LOGGER = logging.getLogger(__name__) -ControllerData = NamedTuple( - "ControllerData", - ( - ("controller", pv.VeraController), - ("devices", DefaultDict[str, List[pv.VeraDevice]]), - ("scenes", List[pv.VeraScene]), - ), -) +class ControllerData(NamedTuple): + """Controller data.""" + + controller: pv.VeraController + devices: DefaultDict[str, List[pv.VeraDevice]] + scenes: List[pv.VeraScene] def get_configured_platforms(controller_data: ControllerData) -> Set[str]: diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py index 3f69745c6ee1ea..2b1827219122df 100644 --- a/homeassistant/components/vera/config_flow.py +++ b/homeassistant/components/vera/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Vera.""" import logging import re -from typing import List +from typing import List, cast import pyvera as pv from requests.exceptions import RequestException @@ -20,7 +20,7 @@ def str_to_int_list(data: str) -> List[str]: """Convert a string to an int list.""" if isinstance(str, list): - return data + return cast(List[str], data) return [s for s in LIST_REGEX.split(data) if len(s) > 0] @@ -86,7 +86,7 @@ def async_get_options_flow(config_entry) -> OptionsFlowHandler: 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_setup") + return self.async_abort(reason="already_configured") if user_input is not None: return await self.async_step_finish( @@ -124,31 +124,7 @@ async def async_step_finish(self, config: dict): reason="cannot_connect", description_placeholders={"base_url": base_url} ) - domain_entries = self.hass.config_entries.async_entries(DOMAIN) - existing_config_entry = next( - iter( - [ - entry - for entry in domain_entries - if entry.unique_id == controller.serial_number - ] - ), - None, - ) - - if existing_config_entry: - _LOGGER.debug( - "Updating existing import config for %s", controller.serial_number - ) - # Note: the options get updated in async_setup_entry() - self.hass.config_entries.async_update_entry( - entry=existing_config_entry, data=config, - ) - - # Integration does not support multiple controllers yet. - if domain_entries: - return self.async_abort(reason="already_setup") - await self.async_set_unique_id(controller.serial_number) + self._abort_if_unique_id_configured(config) return self.async_create_entry(title=base_url, data=config) diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py index 82188b49a46b44..60ebeeb156655e 100644 --- a/homeassistant/components/vera/sensor.py +++ b/homeassistant/components/vera/sensor.py @@ -7,7 +7,7 @@ from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN, ENTITY_ID_FORMAT from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.util import convert diff --git a/homeassistant/components/vera/strings.json b/homeassistant/components/vera/strings.json index 8c9db462f1c22e..d55b20477a1bd8 100644 --- a/homeassistant/components/vera/strings.json +++ b/homeassistant/components/vera/strings.json @@ -2,7 +2,7 @@ "config": { "title": "Vera", "abort": { - "already_setup": "A controller is already configured.", + "already_configured": "A controller is already configured.", "cannot_connect": "Could not connect to controller with url {base_url}" }, "step": { @@ -24,7 +24,7 @@ "step": { "init": { "title": "Vera controller options", - "description": "See the vera documentation for details on optional parameters: https://www.home-assistant.io/integrations/vera/. Note: Any changes here will need a restart to the home assistant server.", + "description": "See the vera documentation for details on optional parameters: https://www.home-assistant.io/integrations/vera/. Note: Any changes here will need a restart to the home assistant server. To clear values, provide a space.", "data": { "lights": "Vera switch device ids to treat as lights in Home Assistant.", "exclude": "Vera device ids to exclude from Home Assistant." diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index 09888e61c14181..bc559b945f0dce 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -55,7 +55,7 @@ async def test_aync_step_user_success(hass: HomeAssistant) -> None: assert entries -async def test_async_step_user_already_setup(hass: HomeAssistant) -> None: +async def test_async_step_user_already_configured(hass: HomeAssistant) -> None: """Test function.""" entry = MockConfigEntry(domain=DOMAIN, data={}, options={}, unique_id="12345") entry.add_to_hass(hass) @@ -68,7 +68,7 @@ async def test_async_step_user_already_setup(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("type") == RESULT_TYPE_ABORT - assert result.get("reason") == "already_setup" + assert result.get("reason") == "already_configured" async def test_aync_step_import_success(hass: HomeAssistant) -> None: @@ -115,7 +115,7 @@ async def test_async_step_import_alredy_setup(hass: HomeAssistant) -> None: data={CONF_CONTROLLER: "http://localhost:445"}, ) assert result.get("type") == RESULT_TYPE_ABORT - assert result.get("reason") == "already_setup" + assert result.get("reason") == "already_configured" async def test_async_step_finish_error(hass: HomeAssistant) -> None: @@ -162,9 +162,11 @@ async def test_options(hass): """Test updating options.""" base_url = "http://127.0.0.1/" entry = MockConfigEntry( - domain=DOMAIN, title=base_url, data={CONF_CONTROLLER: "http://127.0.0.1/"}, + domain=DOMAIN, + title=base_url, + data={CONF_CONTROLLER: "http://127.0.0.1/"}, + options={CONF_LIGHTS: [1, 2, 3]}, ) - entry.options[CONF_LIGHTS] = [1, 2, 3] entry.add_to_hass(hass) result = await hass.config_entries.options.async_init( From 3d12eee40459674d2dd8ad223799bf0d8deecf89 Mon Sep 17 00:00:00 2001 From: Robbie Van Gorkom Date: Thu, 2 Apr 2020 20:00:42 -0700 Subject: [PATCH 21/21] Addressing PR feedback. --- homeassistant/components/vera/__init__.py | 2 +- homeassistant/components/vera/config_flow.py | 2 +- homeassistant/components/vera/strings.json | 3 - tests/components/vera/common.py | 50 +++++----- tests/components/vera/test_config_flow.py | 96 +++++++------------- tests/components/vera/test_init.py | 26 +++--- 6 files changed, 80 insertions(+), 99 deletions(-) diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 25ed3824e06b51..c98833a7daa014 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -81,7 +81,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ), ) - base_url = config_entry.data.get(CONF_CONTROLLER) + base_url = config_entry.data[CONF_CONTROLLER] light_ids = config_entry.options.get(CONF_LIGHTS, []) exclude_ids = config_entry.options.get(CONF_EXCLUDE, []) diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py index 2b1827219122df..3d2b30f1079fc7 100644 --- a/homeassistant/components/vera/config_flow.py +++ b/homeassistant/components/vera/config_flow.py @@ -112,7 +112,7 @@ async def async_step_import(self, config: dict): async def async_step_finish(self, config: dict): """Validate and create config entry.""" - base_url = config[CONF_CONTROLLER] = config.get(CONF_CONTROLLER).rstrip("/") + base_url = config[CONF_CONTROLLER] = config[CONF_CONTROLLER].rstrip("/") controller = pv.VeraController(base_url) # Verify the controller is online and get the serial number. diff --git a/homeassistant/components/vera/strings.json b/homeassistant/components/vera/strings.json index d55b20477a1bd8..d8dec2c40cfa70 100644 --- a/homeassistant/components/vera/strings.json +++ b/homeassistant/components/vera/strings.json @@ -18,9 +18,6 @@ } }, "options": { - "abort": { - "file_based_config": "Configuration for this Vera controller is handled in configuration.yml. Please make changes there." - }, "step": { "init": { "title": "Vera controller options", diff --git a/tests/components/vera/common.py b/tests/components/vera/common.py index 3dbcade1322a93..5574c93c515d09 100644 --- a/tests/components/vera/common.py +++ b/tests/components/vera/common.py @@ -5,31 +5,38 @@ from mock import MagicMock import pyvera as pv -from homeassistant import config_entries from homeassistant.components.vera.const import CONF_CONTROLLER, DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry + SetupCallback = Callable[[pv.VeraController, dict], None] -ControllerData = NamedTuple( - "ControllerData", (("controller", pv.VeraController), ("update_callback", Callable)) -) -ComponentData = NamedTuple("ComponentData", (("controller_data", ControllerData),),) +class ControllerData(NamedTuple): + """Test data about a specific vera controller.""" + + controller: pv.VeraController + update_callback: Callable + + +class ComponentData(NamedTuple): + """Test data about the vera component.""" -ControllerConfig = NamedTuple( - "ControllerConfig", - ( - ("config", Dict), - ("options", Dict), - ("config_from_file", bool), - ("serial_number", str), - ("devices", Tuple[pv.VeraDevice, ...]), - ("scenes", Tuple[pv.VeraScene, ...]), - ("setup_callback", SetupCallback), - ), -) + controller_data: ControllerData + + +class ControllerConfig(NamedTuple): + """Test config for mocking a vera controller.""" + + config: Dict + options: Dict + config_from_file: bool + serial_number: str + devices: Tuple[pv.VeraDevice, ...] + scenes: Tuple[pv.VeraScene, ...] + setup_callback: SetupCallback def new_simple_controller_config( @@ -103,11 +110,12 @@ async def configure_component( # Setup component through config flow. if not controller_config.config_from_file: - await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data=component_config, + entry = MockConfigEntry( + domain=DOMAIN, data=component_config, options={}, unique_id="12345" ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() update_callback = ( diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index bc559b945f0dce..52ba55b509c8ac 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -6,7 +6,6 @@ from homeassistant import config_entries, data_entry_flow from homeassistant.components.vera import CONF_CONTROLLER, DOMAIN -from homeassistant.components.vera.config_flow import VeraFlowHandler from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( @@ -18,8 +17,8 @@ from tests.common import MockConfigEntry -async def test_aync_step_user_success(hass: HomeAssistant) -> None: - """Test function.""" +async def test_async_step_user_success(hass: HomeAssistant) -> None: + """Test user step success.""" with patch("pyvera.VeraController") as vera_controller_class_mock: controller = MagicMock() controller.refresh_data = MagicMock() @@ -29,50 +28,45 @@ async def test_aync_step_user_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM - assert result.get("step_id") == config_entries.SOURCE_USER + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == config_entries.SOURCE_USER - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data={ + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ CONF_CONTROLLER: "http://127.0.0.1:123/", CONF_LIGHTS: "12 13", CONF_EXCLUDE: "14 15", }, ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY - assert result.get("title") == "http://127.0.0.1:123" - assert result.get("data") == { + 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_USER, CONF_LIGHTS: ["12", "13"], CONF_EXCLUDE: ["14", "15"], } - assert result.get("result").unique_id == controller.serial_number + assert result["result"].unique_id == controller.serial_number entries = hass.config_entries.async_entries(DOMAIN) assert entries async def test_async_step_user_already_configured(hass: HomeAssistant) -> None: - """Test function.""" + """Test user step with entry already configured.""" entry = MockConfigEntry(domain=DOMAIN, data={}, options={}, unique_id="12345") entry.add_to_hass(hass) - handler = VeraFlowHandler() - handler.context = {} - handler.hass = hass - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_ABORT - assert result.get("reason") == "already_configured" + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" -async def test_aync_step_import_success(hass: HomeAssistant) -> None: - """Test function.""" +async def test_async_step_import_success(hass: HomeAssistant) -> None: + """Test import step success.""" with patch("pyvera.VeraController") as vera_controller_class_mock: controller = MagicMock() controller.refresh_data = MagicMock() @@ -85,24 +79,20 @@ async def test_aync_step_import_success(hass: HomeAssistant) -> None: data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY - assert result.get("title") == "http://127.0.0.1:123" - assert result.get("data") == { + 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, } - assert result.get("result").unique_id == controller.serial_number + assert result["result"].unique_id == controller.serial_number async def test_async_step_import_alredy_setup(hass: HomeAssistant) -> None: - """Test function.""" + """Test import step with entry already setup.""" entry = MockConfigEntry(domain=DOMAIN, data={}, options={}, unique_id="12345") entry.add_to_hass(hass) - handler = VeraFlowHandler() - handler.context = {} - handler.hass = hass - with patch("pyvera.VeraController") as vera_controller_class_mock: controller = MagicMock() controller.refresh_data = MagicMock() @@ -114,12 +104,12 @@ async def test_async_step_import_alredy_setup(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_IMPORT}, data={CONF_CONTROLLER: "http://localhost:445"}, ) - assert result.get("type") == RESULT_TYPE_ABORT - assert result.get("reason") == "already_configured" + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" async def test_async_step_finish_error(hass: HomeAssistant) -> None: - """Test function.""" + """Test finish step with error.""" with patch("pyvera.VeraController") as vera_controller_class_mock: controller = MagicMock() controller.refresh_data = MagicMock(side_effect=RequestException()) @@ -131,33 +121,13 @@ async def test_async_step_finish_error(hass: HomeAssistant) -> None: data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, ) - assert result.get("type") == "abort" - assert result.get("reason") == "cannot_connect" - assert result.get("description_placeholders") == { + assert result["type"] == "abort" + assert result["reason"] == "cannot_connect" + assert result["description_placeholders"] == { "base_url": "http://127.0.0.1:123" } -async def test_options_available(hass: HomeAssistant) -> None: - """Test function.""" - with patch("pyvera.VeraController") as vera_controller_class_mock: - controller = MagicMock() - controller.refresh_data = MagicMock() - controller.serial_number = "serial_number_1" - vera_controller_class_mock.return_value = controller - - await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, - ) - entries = hass.config_entries.async_entries(DOMAIN) - - await hass.config_entries.options.async_init( - entries[0].entry_id, context={}, data={} - ) - - async def test_options(hass): """Test updating options.""" base_url = "http://127.0.0.1/" @@ -175,13 +145,15 @@ async def test_options(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "init" - result = await hass.config_entries.options.async_init( - entry.entry_id, - context={"source": "test"}, - data={CONF_LIGHTS: "1,2;3 4 5_6bb7", CONF_EXCLUDE: "8,9;10 11 12_13bb14"}, + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_LIGHTS: "1,2;3 4 5_6bb7", + CONF_EXCLUDE: "8,9;10 11 12_13bb14", + }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result.get("data") == { + assert result["data"] == { CONF_LIGHTS: ["1", "2", "3", "4", "5", "6", "7"], CONF_EXCLUDE: ["8", "9", "10", "11", "12", "13", "14"], } diff --git a/tests/components/vera/test_init.py b/tests/components/vera/test_init.py index 226108a51e9037..a6208726451ba0 100644 --- a/tests/components/vera/test_init.py +++ b/tests/components/vera/test_init.py @@ -3,17 +3,14 @@ import pyvera as pv from requests.exceptions import RequestException -from homeassistant.components.vera import ( - CONF_CONTROLLER, - DOMAIN, - async_setup_entry, - async_unload_entry, -) -from homeassistant.config_entries import ConfigEntry +from homeassistant.components.vera import CONF_CONTROLLER, DOMAIN +from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config +from tests.common import MockConfigEntry + async def test_init( hass: HomeAssistant, vera_component_factory: ComponentFactory @@ -86,7 +83,8 @@ async def test_unload( assert entries for config_entry in entries: - await async_unload_entry(hass, config_entry) + assert await hass.config_entries.async_unload(config_entry.entry_id) + assert config_entry.state == ENTRY_STATE_NOT_LOADED async def test_async_setup_entry_error( @@ -102,7 +100,13 @@ def setup_callback(controller: pv.VeraController) -> None: hass=hass, controller_config=new_simple_controller_config(setup_callback=setup_callback), ) - entry = MagicMock(spec=ConfigEntry) # type: ConfigEntry - entry.data = {CONF_CONTROLLER: "http://127.0.0.1"} - assert not await async_setup_entry(hass, entry) + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_CONTROLLER: "http://127.0.0.1"}, + options={}, + unique_id="12345", + ) + entry.add_to_hass(hass) + + assert not await hass.config_entries.async_setup(entry.entry_id)