diff --git a/CODEOWNERS b/CODEOWNERS index 4d4c7d3d900736c7598d0cab999ee9d4e82a5737..5cbf0d411a0f23e82b2f76e260c67021da9039c1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -409,6 +409,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 diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 1c9d412d974ddd033d8afab88be235f8c7d8e768..c98833a7daa014d15c36dcf42525a6eee6b5f3c2 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 @@ -6,6 +7,8 @@ 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 ( ATTR_ARMED, ATTR_BATTERY_LEVEL, @@ -15,26 +18,23 @@ from homeassistant.const import ( CONF_LIGHTS, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.helpers import config_validation as cv, discovery +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 -_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" +from .common import ControllerData, get_configured_platforms +from .config_flow import new_options +from .const import ( + ATTR_CURRENT_ENERGY_KWH, + ATTR_CURRENT_POWER_W, + CONF_CONTROLLER, + DOMAIN, + VERA_ID_FORMAT, +) -VERA_DEVICES = "vera_devices" -VERA_SCENES = "vera_scenes" +_LOGGER = logging.getLogger(__name__) VERA_ID_LIST_SCHEMA = vol.Schema([int]) @@ -51,42 +51,53 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -VERA_COMPONENTS = [ - "binary_sensor", - "sensor", - "light", - "switch", - "lock", - "climate", - "cover", - "scene", -] +async def async_setup(hass: HomeAssistant, base_config: dict) -> bool: + """Set up for Vera controllers.""" + config = base_config.get(DOMAIN) -def setup(hass, base_config): - """Set up for Vera devices.""" + if not config: + return True - def stop_subscription(event): - """Shutdown Vera subscriptions and subscription thread on exit.""" - _LOGGER.info("Shutting down subscriptions") - hass.data[VERA_CONTROLLER].stop() + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config, + ) + ) - config = base_config.get(DOMAIN) + return True - # Get Vera specific configuration. - base_url = config.get(CONF_CONTROLLER) - light_ids = config.get(CONF_LIGHTS) - exclude_ids = config.get(CONF_EXCLUDE) + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Do setup of vera.""" + # 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=new_options( + config_entry.data.get(CONF_LIGHTS, []), + config_entry.data.get(CONF_EXCLUDE, []), + ), + ) + + base_url = config_entry.data[CONF_CONTROLLER] + light_ids = config_entry.options.get(CONF_LIGHTS, []) + exclude_ids = config_entry.options.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) + controller = veraApi.VeraController(base_url) + controller.start() + + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, + lambda event: hass.async_add_executor_job(controller.stop), + ) 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") @@ -102,15 +113,35 @@ def setup(hass, base_config): 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 - for component in VERA_COMPONENTS: - discovery.load_platform(hass, component, DOMAIN, {}, base_config) + controller_data = ControllerData( + controller=controller, devices=vera_devices, scenes=vera_scenes + ) + + hass.data[DOMAIN] = controller_data + + # 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) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload Withings config entry.""" + controller_data = hass.data[DOMAIN] + + 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/binary_sensor.py b/homeassistant/components/vera/binary_sensor.py index 061d2c5c99aef45df4c688159c97c7f4fead7bcb..621dc09930d99f439c71413b6f2aa2e9de54331a 100644 --- a/homeassistant/components/vera/binary_sensor.py +++ b/homeassistant/components/vera/binary_sensor.py @@ -1,21 +1,34 @@ """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 . import VeraDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Perform the setup for Vera controller devices.""" - add_entities( +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up the sensor config entry.""" + controller_data = hass.data[DOMAIN] + async_add_entities( [ - VeraBinarySensor(device, hass.data[VERA_CONTROLLER]) - for device in hass.data[VERA_DEVICES]["binary_sensor"] - ], - True, + 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 60e73d48978cdb1f6b9be24e0fcfe0516dc70df1..520c3b516df34f9d9960911420c82fc96df4cbf7 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,14 @@ from homeassistant.components.climate.const import ( 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 . import VeraDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -25,14 +34,18 @@ SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE 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( +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up the sensor config entry.""" + controller_data = hass.data[DOMAIN] + async_add_entities( [ - VeraThermostat(device, hass.data[VERA_CONTROLLER]) - for device in hass.data[VERA_DEVICES]["climate"] - ], - True, + 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 new file mode 100644 index 0000000000000000000000000000000000000000..cdfdff404ec9117605bc1ba7577476647fedd006 --- /dev/null +++ b/homeassistant/components/vera/common.py @@ -0,0 +1,29 @@ +"""Common vera code.""" +import logging +from typing import DefaultDict, List, NamedTuple, Set + +import pyvera as pv + +from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +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]: + """Get configured platforms for a controller.""" + platforms = [] + for platform in controller_data.devices: + 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 0000000000000000000000000000000000000000..3d2b30f1079fc7a08bce509f599d1cc7c3df9c63 --- /dev/null +++ b/homeassistant/components/vera/config_flow.py @@ -0,0 +1,130 @@ +"""Config flow for Vera.""" +import logging +import re +from typing import List, cast + +import pyvera as pv +from requests.exceptions import RequestException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE +from homeassistant.core import callback + +from .const import CONF_CONTROLLER, DOMAIN + +LIST_REGEX = re.compile("[^0-9]+") +_LOGGER = logging.getLogger(__name__) + + +def str_to_int_list(data: str) -> List[str]: + """Convert a string to an int list.""" + if isinstance(str, list): + return cast(List[str], data) + + return [s for s in LIST_REGEX.split(data) if len(s) > 0] + + +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[str], exclude: List[str]) -> 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.""" + + 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=options_data(user_input),) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema(options_schema(self.config_entry.options)), + ) + + +class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Vera config flow.""" + + @staticmethod + @callback + def async_get_options_flow(config_entry) -> OptionsFlowHandler: + """Get the options flow.""" + return OptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input: dict = None): + """Handle user initiated flow.""" + if self.hass.config_entries.async_entries(DOMAIN): + return self.async_abort(reason="already_configured") + + if user_input is not None: + return await self.async_step_finish( + { + **user_input, + **options_data(user_input), + **{CONF_SOURCE: config_entries.SOURCE_USER}, + } + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {**{vol.Required(CONF_CONTROLLER): str}, **options_schema()} + ), + ) + + async def async_step_import(self, config: dict): + """Handle a flow initialized by import.""" + 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.""" + base_url = config[CONF_CONTROLLER] = config[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} + ) + + 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/const.py b/homeassistant/components/vera/const.py new file mode 100644 index 0000000000000000000000000000000000000000..c4f1d0efa3a0b2379a87b3defc71acc3fcebd348 --- /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 b90dd8a053112daed699d4a47e691ebb38d1b191..0d0edb841c1ec041ab4c2fe05fca7aa7ed0c5d2d 100644 --- a/homeassistant/components/vera/cover.py +++ b/homeassistant/components/vera/cover.py @@ -1,21 +1,35 @@ """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 . import VeraDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Vera covers.""" - add_entities( +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up the sensor config entry.""" + controller_data = hass.data[DOMAIN] + async_add_entities( [ - VeraCover(device, hass.data[VERA_CONTROLLER]) - for device in hass.data[VERA_DEVICES]["cover"] - ], - True, + 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 fee992356816e3c0d49cb2f9c4240efec6686fda..877fdf51f0a48d3493e9cd11638a3fe8d5e11978 100644 --- a/homeassistant/components/vera/light.py +++ b/homeassistant/components/vera/light.py @@ -1,29 +1,39 @@ """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 . import VeraDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Vera lights.""" - add_entities( +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up the sensor config entry.""" + controller_data = hass.data[DOMAIN] + async_add_entities( [ - VeraLight(device, hass.data[VERA_CONTROLLER]) - for device in hass.data[VERA_DEVICES]["light"] - ], - True, + 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 23b62bb03314667ccdd01cb1e5fe52adf53a4676..da3c432a6afa91ea2ca088f362e0e379134b6a44 100644 --- a/homeassistant/components/vera/lock.py +++ b/homeassistant/components/vera/lock.py @@ -1,10 +1,19 @@ """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 . import VeraDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -12,14 +21,18 @@ ATTR_LAST_USER_NAME = "changed_by_name" ATTR_LOW_BATTERY = "low_battery" -def setup_platform(hass, config, add_entities, discovery_info=None): - """Find and return Vera locks.""" - add_entities( +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up the sensor config entry.""" + controller_data = hass.data[DOMAIN] + async_add_entities( [ - VeraLock(device, hass.data[VERA_CONTROLLER]) - for device in hass.data[VERA_DEVICES]["lock"] - ], - True, + VeraLock(device, controller_data.controller) + for device in controller_data.devices.get(PLATFORM_DOMAIN) + ] ) diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json index 63102c2968786217d78418b60a3e0cb647aec125..4f585d964a86a77f041cb70b437635639d92c910 100644 --- a/homeassistant/components/vera/manifest.json +++ b/homeassistant/components/vera/manifest.json @@ -1,8 +1,11 @@ { "domain": "vera", "name": "Vera", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/vera", "requirements": ["pyvera==0.3.7"], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@vangorra" + ] } diff --git a/homeassistant/components/vera/scene.py b/homeassistant/components/vera/scene.py index af5266ed4b3d7b40741687f7fcc96105e962f30d..7d09e2488938816b8b67b0dd7583ddd91bf421e2 100644 --- a/homeassistant/components/vera/scene.py +++ b/homeassistant/components/vera/scene.py @@ -1,22 +1,30 @@ """Support for Vera scenes.""" import logging +from typing import Callable, List 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 . import VERA_CONTROLLER, VERA_ID_FORMAT, VERA_SCENES +from .const import DOMAIN, VERA_ID_FORMAT _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Vera scenes.""" - add_entities( +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up the sensor config entry.""" + controller_data = hass.data[DOMAIN] + async_add_entities( [ - VeraScene(scene, hass.data[VERA_CONTROLLER]) - for scene in hass.data[VERA_SCENES] - ], - True, + 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 9ac0a36ff9cd0007802a3dff4ab1c15de768789d..60ebeeb156655e2c5a760d2adfd08e67fe7d93a4 100644 --- a/homeassistant/components/vera/sensor.py +++ b/homeassistant/components/vera/sensor.py @@ -1,29 +1,37 @@ """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.components.sensor import DOMAIN as PLATFORM_DOMAIN, ENTITY_ID_FORMAT +from homeassistant.config_entries import ConfigEntry 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 -from . import VERA_CONTROLLER, VERA_DEVICES, VeraDevice +from . import VeraDevice +from .const import DOMAIN _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( +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up the sensor config entry.""" + controller_data = hass.data[DOMAIN] + async_add_entities( [ - VeraSensor(device, hass.data[VERA_CONTROLLER]) - for device in hass.data[VERA_DEVICES]["sensor"] - ], - True, + VeraSensor(device, controller_data.controller) + for device in controller_data.devices.get(PLATFORM_DOMAIN) + ] ) diff --git a/homeassistant/components/vera/strings.json b/homeassistant/components/vera/strings.json new file mode 100644 index 0000000000000000000000000000000000000000..d8dec2c40cfa700aef48fbe56e44504f138332dc --- /dev/null +++ b/homeassistant/components/vera/strings.json @@ -0,0 +1,32 @@ +{ + "config": { + "title": "Vera", + "abort": { + "already_configured": "A controller is already configured.", + "cannot_connect": "Could not connect to controller with url {base_url}" + }, + "step": { + "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", + "lights": "Vera switch device ids to treat as lights in Home Assistant.", + "exclude": "Vera device ids to exclude from Home Assistant." + } + } + } + }, + "options": { + "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. 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/homeassistant/components/vera/switch.py b/homeassistant/components/vera/switch.py index ab3c3e6adb9555093513a3a72d0c0e44c5993703..a7ae6d45573e41177a8366792e128f898c06fba1 100644 --- a/homeassistant/components/vera/switch.py +++ b/homeassistant/components/vera/switch.py @@ -1,22 +1,35 @@ """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 . import VeraDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Vera switches.""" - add_entities( +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up the sensor config entry.""" + controller_data = hass.data[DOMAIN] + async_add_entities( [ - VeraSwitch(device, hass.data[VERA_CONTROLLER]) - for device in hass.data[VERA_DEVICES]["switch"] - ], - True, + VeraSwitch(device, controller_data.controller) + for device in controller_data.devices.get(PLATFORM_DOMAIN) + ] ) diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 1584d342db4f59e5e2cec247c5abc97a60869df9..e00cd1b5936b34e833bdaa6a422f02d75da8f7db 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -122,6 +122,7 @@ FLOWS = [ "unifi", "upnp", "velbus", + "vera", "vesync", "vilfo", "vizio", diff --git a/tests/components/vera/common.py b/tests/components/vera/common.py index 649cf9af6a54e852300b0473ee113f3f6c80f756..5574c93c515d0966ee7fd4353e6c40239fb80a37 100644 --- a/tests/components/vera/common.py +++ b/tests/components/vera/common.py @@ -1,47 +1,91 @@ """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 +from tests.common import MockConfigEntry -class ComponentData(NamedTuple): - """Component data.""" +SetupCallback = Callable[[pv.VeraController, dict], None] + + +class ControllerData(NamedTuple): + """Test data about a specific vera controller.""" + + controller: pv.VeraController + update_callback: Callable - controller: VeraController + +class ComponentData(NamedTuple): + """Test data about the vera component.""" + + 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( + config: dict = None, + options: dict = None, + config_from_file=False, + serial_number="1111", + devices: Tuple[pv.VeraDevice, ...] = (), + scenes: Tuple[pv.VeraScene, ...] = (), + setup_callback: SetupCallback = None, +) -> ControllerConfig: + """Create simple contorller config.""" + return ControllerConfig( + config=config or {CONF_CONTROLLER: "http://127.0.0.1:123"}, + options=options, + config_from_file=config_from_file, + 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): + """Initialize the factory.""" + 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}, + component_config = { + **(controller_config.config or {}), + **(controller_config.options or {}), } - controller = MagicMock(spec=VeraController) # type: VeraController - controller.base_url = controller_url + controller = MagicMock(spec=pv.VeraController) # type: pv.VeraController + controller.base_url = component_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 +93,39 @@ class ComponentFactory: controller.get_devices.reset_mock() controller.get_scenes.reset_mock() - if setup_callback: - setup_callback(controller, hass_config) + if controller_config.setup_callback: + controller_config.setup_callback(controller) + + self.vera_controller_class_mock.return_value = controller - def init_controller(base_url: str) -> list: - nonlocal controller - return [controller, True] + hass_config = {} - self.init_controller_mock.side_effect = init_controller + # 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() - return ComponentData(controller=controller) + # Setup component through config flow. + if not controller_config.config_from_file: + 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 = ( + 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 b94a40135d8b09c169568d0adba956810676502f..2c15d3e4182fbeed60de90a85e04e72d065c48b7 100644 --- a/tests/components/vera/conftest.py +++ b/tests/components/vera/conftest.py @@ -9,5 +9,5 @@ from .common import ComponentFactory @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 2c2e2b8638818a838dfb739002b9272068b5761d..72651d6eda4f7490529e49f1cbfd0a27df23eb6b 100644 --- a/tests/components/vera/test_binary_sensor.py +++ b/tests/components/vera/test_binary_sensor.py @@ -1,38 +1,36 @@ """Vera tests.""" from unittest.mock import MagicMock -from pyvera import VeraBinarySensor +import pyvera as pv from homeassistant.core import HomeAssistant -from .common import ComponentFactory +from .common import ComponentFactory, new_simple_controller_config 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" 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 c27a72865fd995f2a1a4728082444555c87035fc..9e5fa983ed053fc8405b39d4f100642bfa11f001 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, @@ -13,17 +13,17 @@ from homeassistant.components.climate.const import ( ) from homeassistant.core import HomeAssistant -from .common import ComponentFactory +from .common import ComponentFactory, new_simple_controller_config 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" @@ -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 @@ -123,24 +123,26 @@ 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) -> 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_config_flow.py b/tests/components/vera/test_config_flow.py new file mode 100644 index 0000000000000000000000000000000000000000..52ba55b509c8ac46585a613550194160b0adaa5f --- /dev/null +++ b/tests/components/vera/test_config_flow.py @@ -0,0 +1,159 @@ +"""Vera tests.""" +from unittest.mock import MagicMock + +from mock import patch +from requests.exceptions import RequestException + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.vera import CONF_CONTROLLER, DOMAIN +from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from tests.common import MockConfigEntry + + +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() + controller.serial_number = "serial_number_0" + vera_controller_class_mock.return_value = controller + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": 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_configure( + result["flow_id"], + user_input={ + CONF_CONTROLLER: "http://127.0.0.1:123/", + CONF_LIGHTS: "12 13", + CONF_EXCLUDE: "14 15", + }, + ) + 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["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 user step with entry already configured.""" + entry = MockConfigEntry(domain=DOMAIN, data={}, options={}, unique_id="12345") + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_import_success(hass: HomeAssistant) -> None: + """Test import step success.""" + with patch("pyvera.VeraController") as vera_controller_class_mock: + controller = MagicMock() + controller.refresh_data = MagicMock() + controller.serial_number = "serial_number_1" + vera_controller_class_mock.return_value = controller + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, + ) + + 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["result"].unique_id == controller.serial_number + + +async def test_async_step_import_alredy_setup(hass: HomeAssistant) -> None: + """Test import step with entry already setup.""" + entry = MockConfigEntry(domain=DOMAIN, data={}, options={}, unique_id="12345") + entry.add_to_hass(hass) + + 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={CONF_CONTROLLER: "http://localhost:445"}, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_finish_error(hass: HomeAssistant) -> None: + """Test finish step with error.""" + 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 + + 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["type"] == "abort" + assert result["reason"] == "cannot_connect" + assert result["description_placeholders"] == { + "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/"}, + options={CONF_LIGHTS: [1, 2, 3]}, + ) + entry.add_to_hass(hass) + + 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.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["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_cover.py b/tests/components/vera/test_cover.py index 79cb4adedfbc9272a19756f077b9795ee5e199c0..62cd47f831cdc3e09e09106c1ac02305a51df423 100644 --- a/tests/components/vera/test_cover.py +++ b/tests/components/vera/test_cover.py @@ -1,30 +1,30 @@ """Vera tests.""" from unittest.mock import MagicMock -from pyvera import CATEGORY_CURTAIN, VeraCurtain +import pyvera as pv from homeassistant.core import HomeAssistant -from .common import ComponentFactory +from .common import ComponentFactory, new_simple_controller_config 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" 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 9ff6cb4058b3c0d0d32accc6aeb381ff818dbe7b..a6208726451ba059022507f42e42404bbb2c27d5 100644 --- a/tests/components/vera/test_init.py +++ b/tests/components/vera/test_init.py @@ -1,78 +1,112 @@ """Vera tests.""" -from unittest.mock import MagicMock - -from pyvera import ( - VeraArmableDevice, - VeraBinarySensor, - VeraController, - VeraCurtain, - VeraDevice, - VeraDimmer, - VeraLock, - VeraScene, - VeraSceneController, - VeraSensor, - VeraSwitch, - VeraThermostat, -) - -from homeassistant.components.vera import ( - CONF_EXCLUDE, - CONF_LIGHTS, - DOMAIN, - VERA_DEVICES, -) +from asynctest import MagicMock +import pyvera as pv +from requests.exceptions import RequestException + +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 +from .common import ComponentFactory, new_simple_controller_config + +from tests.common import MockConfigEntry -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 +async def test_init( + 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=False, + serial_number="first_serial", + devices=(vera_device1,), + ), + ) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + entry1 = entity_registry.async_get(entity1_id) -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 + assert entry1 -async def test_init( +async def test_init_from_file( 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=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, - 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( + config={CONF_CONTROLLER: "http://127.0.0.1:111"}, + config_from_file=True, + 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=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 + + await vera_component_factory.configure_component( + hass=hass, controller_config=new_simple_controller_config() + ) + + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + + for config_entry in entries: + 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( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + + def setup_callback(controller: pv.VeraController) -> 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 = 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) diff --git a/tests/components/vera/test_light.py b/tests/components/vera/test_light.py index fa63ce6345444411fcc3e248aec9d40d96d83b2e..fefa07ffa6ee40525ec8ba0475a23dcf2adf51ca 100644 --- a/tests/components/vera/test_light.py +++ b/tests/components/vera/test_light.py @@ -1,22 +1,22 @@ """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 -from .common import ComponentFactory +from .common import ComponentFactory, new_simple_controller_config 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]) @@ -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 362bdbeddc0fdef3d36434d7c1ac6950b52ce97a..d1b2209294a375ba84dbc2bbc2d2f9f03711e1fa 100644 --- a/tests/components/vera/test_lock.py +++ b/tests/components/vera/test_lock.py @@ -1,30 +1,30 @@ """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 -from .common import ComponentFactory +from .common import ComponentFactory, new_simple_controller_config 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" 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 136227ffa7126e6c0d433ae77c3540d1a05d83cf..732a331681bdb234e3f297a57c836c65f15c38cd 100644 --- a/tests/components/vera/test_scene.py +++ b/tests/components/vera/test_scene.py @@ -1,24 +1,24 @@ """Vera tests.""" from unittest.mock import MagicMock -from pyvera import VeraScene +import pyvera as pv from homeassistant.core import HomeAssistant -from .common import ComponentFactory +from .common import ComponentFactory, new_simple_controller_config 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" 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 9e84815d636230d669bda55afb7d5655c9f777ec..c915c5ead0fd173e0c1e08bb0c1fa0b63217545e 100644 --- a/tests/components/vera/test_sensor.py +++ b/tests/components/vera/test_sensor.py @@ -2,21 +2,12 @@ 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 -from .common import ComponentFactory +from .common import ComponentFactory, new_simple_controller_config async def run_sensor_test( @@ -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 @@ -37,10 +28,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) @@ -57,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) -> 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, @@ -77,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")), ) @@ -90,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", @@ -104,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", @@ -118,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, @@ -132,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", @@ -144,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) -> None: controller.get_devices()[0].is_trippable = True await run_sensor_test( @@ -162,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) -> None: controller.get_devices()[0].is_trippable = False await run_sensor_test( @@ -179,21 +172,21 @@ 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" 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" + 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" diff --git a/tests/components/vera/test_switch.py b/tests/components/vera/test_switch.py index ba09068e7e602fb14265351c8dbf1005bcc91f84..c41afad4759f8f3f909933ed4fbf483791245a54 100644 --- a/tests/components/vera/test_switch.py +++ b/tests/components/vera/test_switch.py @@ -1,29 +1,29 @@ """Vera tests.""" from unittest.mock import MagicMock -from pyvera import CATEGORY_SWITCH, VeraSwitch +import pyvera as pv from homeassistant.core import HomeAssistant -from .common import ComponentFactory +from .common import ComponentFactory, new_simple_controller_config 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" 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"