Add support for multiple vera controller hubs (#33613)

This commit is contained in:
Robert Van Gorkom 2020-09-14 20:06:52 -07:00 committed by GitHub
parent 938e06c00e
commit 903afb62d0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 323 additions and 149 deletions

View file

@ -2,6 +2,7 @@
import asyncio
from collections import defaultdict
import logging
from typing import Type
import pyvera as veraApi
from requests.exceptions import RequestException
@ -19,17 +20,25 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import convert, slugify
from homeassistant.util.dt import utc_from_timestamp
from .common import ControllerData, SubscriptionRegistry, get_configured_platforms
from .common import (
ControllerData,
SubscriptionRegistry,
get_configured_platforms,
get_controller_data,
set_controller_data,
)
from .config_flow import fix_device_id_list, new_options
from .const import (
ATTR_CURRENT_ENERGY_KWH,
ATTR_CURRENT_POWER_W,
CONF_CONTROLLER,
CONF_LEGACY_UNIQUE_ID,
DOMAIN,
VERA_ID_FORMAT,
)
@ -54,6 +63,8 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, base_config: dict) -> bool:
"""Set up for Vera controllers."""
hass.data[DOMAIN] = {}
config = base_config.get(DOMAIN)
if not config:
@ -107,10 +118,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
all_devices = await hass.async_add_executor_job(controller.get_devices)
all_scenes = await hass.async_add_executor_job(controller.get_scenes)
except RequestException:
except RequestException as exception:
# There was a network related error connecting to the Vera controller.
_LOGGER.exception("Error communicating with Vera API")
return False
raise ConfigEntryNotReady from exception
# Exclude devices unwanted by user.
devices = [device for device in all_devices if device.device_id not in exclude_ids]
@ -118,20 +129,21 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
vera_devices = defaultdict(list)
for device in devices:
device_type = map_vera_device(device, light_ids)
if device_type is None:
continue
vera_devices[device_type].append(device)
if device_type is not None:
vera_devices[device_type].append(device)
vera_scenes = []
for scene in all_scenes:
vera_scenes.append(scene)
controller_data = ControllerData(
controller=controller, devices=vera_devices, scenes=vera_scenes
controller=controller,
devices=vera_devices,
scenes=vera_scenes,
config_entry=config_entry,
)
hass.data[DOMAIN] = controller_data
set_controller_data(hass, config_entry, controller_data)
# Forward the config data to the necessary platforms.
for platform in get_configured_platforms(controller_data):
@ -144,7 +156,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload Withings config entry."""
controller_data: ControllerData = hass.data[DOMAIN]
controller_data: ControllerData = get_controller_data(hass, config_entry)
tasks = [
hass.config_entries.async_forward_entry_unload(config_entry, platform)
@ -159,43 +171,52 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
def map_vera_device(vera_device, remap):
"""Map vera classes to Home Assistant types."""
if isinstance(vera_device, veraApi.VeraDimmer):
return "light"
if isinstance(vera_device, veraApi.VeraBinarySensor):
return "binary_sensor"
if isinstance(vera_device, veraApi.VeraSensor):
return "sensor"
if isinstance(vera_device, veraApi.VeraArmableDevice):
return "switch"
if isinstance(vera_device, veraApi.VeraLock):
return "lock"
if isinstance(vera_device, veraApi.VeraThermostat):
return "climate"
if isinstance(vera_device, veraApi.VeraCurtain):
return "cover"
if isinstance(vera_device, veraApi.VeraSceneController):
return "sensor"
if isinstance(vera_device, veraApi.VeraSwitch):
if vera_device.device_id in remap:
type_map = {
veraApi.VeraDimmer: "light",
veraApi.VeraBinarySensor: "binary_sensor",
veraApi.VeraSensor: "sensor",
veraApi.VeraArmableDevice: "switch",
veraApi.VeraLock: "lock",
veraApi.VeraThermostat: "climate",
veraApi.VeraCurtain: "cover",
veraApi.VeraSceneController: "sensor",
veraApi.VeraSwitch: "switch",
}
def map_special_case(instance_class: Type, entity_type: str) -> str:
if instance_class is veraApi.VeraSwitch and vera_device.device_id in remap:
return "light"
return "switch"
return None
return entity_type
return next(
iter(
map_special_case(instance_class, entity_type)
for instance_class, entity_type in type_map.items()
if isinstance(vera_device, instance_class)
),
None,
)
class VeraDevice(Entity):
"""Representation of a Vera device entity."""
def __init__(self, vera_device, controller):
def __init__(self, vera_device, controller_data: ControllerData):
"""Initialize the device."""
self.vera_device = vera_device
self.controller = controller
self.controller = controller_data.controller
self._name = self.vera_device.name
# Append device id to prevent name clashes in HA.
self.vera_id = VERA_ID_FORMAT.format(
slugify(vera_device.name), vera_device.device_id
slugify(vera_device.name), vera_device.vera_device_id
)
if controller_data.config_entry.data.get(CONF_LEGACY_UNIQUE_ID):
self._unique_id = str(self.vera_device.vera_device_id)
else:
self._unique_id = f"vera_{controller_data.config_entry.unique_id}_{self.vera_device.vera_device_id}"
async def async_added_to_hass(self):
"""Subscribe to updates."""
self.controller.register(self.vera_device, self._update_callback)
@ -254,4 +275,4 @@ class VeraDevice(Entity):
The Vera assigns a unique and immutable ID number to each device.
"""
return str(self.vera_device.vera_device_id)
return self._unique_id

View file

@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
from . import VeraDevice
from .const import DOMAIN
from .common import ControllerData, get_controller_data
_LOGGER = logging.getLogger(__name__)
@ -23,10 +23,10 @@ async def async_setup_entry(
async_add_entities: Callable[[List[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
controller_data = hass.data[DOMAIN]
controller_data = get_controller_data(hass, entry)
async_add_entities(
[
VeraBinarySensor(device, controller_data.controller)
VeraBinarySensor(device, controller_data)
for device in controller_data.devices.get(PLATFORM_DOMAIN)
]
)
@ -35,10 +35,10 @@ async def async_setup_entry(
class VeraBinarySensor(VeraDevice, BinarySensorEntity):
"""Representation of a Vera Binary Sensor."""
def __init__(self, vera_device, controller):
def __init__(self, vera_device, controller_data: ControllerData):
"""Initialize the binary_sensor."""
self._state = False
VeraDevice.__init__(self, vera_device, controller)
VeraDevice.__init__(self, vera_device, controller_data)
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
@property

View file

@ -24,7 +24,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.util import convert
from . import VeraDevice
from .const import DOMAIN
from .common import ControllerData, get_controller_data
_LOGGER = logging.getLogger(__name__)
@ -40,10 +40,10 @@ async def async_setup_entry(
async_add_entities: Callable[[List[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
controller_data = hass.data[DOMAIN]
controller_data = get_controller_data(hass, entry)
async_add_entities(
[
VeraThermostat(device, controller_data.controller)
VeraThermostat(device, controller_data)
for device in controller_data.devices.get(PLATFORM_DOMAIN)
]
)
@ -52,9 +52,9 @@ async def async_setup_entry(
class VeraThermostat(VeraDevice, ClimateEntity):
"""Representation of a Vera Thermostat."""
def __init__(self, vera_device, controller):
def __init__(self, vera_device, controller_data: ControllerData):
"""Initialize the Vera device."""
VeraDevice.__init__(self, vera_device, controller)
VeraDevice.__init__(self, vera_device, controller_data)
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
@property

View file

@ -5,9 +5,12 @@ from typing import DefaultDict, List, NamedTuple, Set
import pyvera as pv
from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.event import call_later
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@ -17,6 +20,7 @@ class ControllerData(NamedTuple):
controller: pv.VeraController
devices: DefaultDict[str, List[pv.VeraDevice]]
scenes: List[pv.VeraScene]
config_entry: ConfigEntry
def get_configured_platforms(controller_data: ControllerData) -> Set[str]:
@ -31,6 +35,20 @@ def get_configured_platforms(controller_data: ControllerData) -> Set[str]:
return set(platforms)
def get_controller_data(
hass: HomeAssistant, config_entry: ConfigEntry
) -> ControllerData:
"""Get controller data from hass data."""
return hass.data[DOMAIN][config_entry.entry_id]
def set_controller_data(
hass: HomeAssistant, config_entry: ConfigEntry, data: ControllerData
) -> None:
"""Set controller data in hass data."""
hass.data[DOMAIN][config_entry.entry_id] = data
class SubscriptionRegistry(pv.AbstractSubscriptionRegistry):
"""Manages polling for data from vera."""

View file

@ -10,8 +10,13 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE
from homeassistant.core import callback
from homeassistant.helpers.entity_registry import EntityRegistry
from .const import CONF_CONTROLLER, DOMAIN
from .const import ( # pylint: disable=unused-import
CONF_CONTROLLER,
CONF_LEGACY_UNIQUE_ID,
DOMAIN,
)
LIST_REGEX = re.compile("[^0-9]+")
_LOGGER = logging.getLogger(__name__)
@ -92,15 +97,13 @@ class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_user(self, user_input: dict = None):
"""Handle user initiated flow."""
if self.hass.config_entries.async_entries(DOMAIN):
return self.async_abort(reason="already_configured")
if user_input is not None:
return await self.async_step_finish(
{
**user_input,
**options_data(user_input),
**{CONF_SOURCE: config_entries.SOURCE_USER},
**{CONF_LEGACY_UNIQUE_ID: False},
}
)
@ -113,8 +116,29 @@ class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_import(self, config: dict):
"""Handle a flow initialized by import."""
# If there are entities with the legacy unique_id, then this imported config
# should also use the legacy unique_id for entity creation.
entity_registry: EntityRegistry = (
await self.hass.helpers.entity_registry.async_get_registry()
)
use_legacy_unique_id = (
len(
[
entry
for entry in entity_registry.entities.values()
if entry.platform == DOMAIN and entry.unique_id.isdigit()
]
)
> 0
)
return await self.async_step_finish(
{**config, **{CONF_SOURCE: config_entries.SOURCE_IMPORT}}
{
**config,
**{CONF_SOURCE: config_entries.SOURCE_IMPORT},
**{CONF_LEGACY_UNIQUE_ID: use_legacy_unique_id},
}
)
async def async_step_finish(self, config: dict):

View file

@ -2,6 +2,7 @@
DOMAIN = "vera"
CONF_CONTROLLER = "vera_controller_url"
CONF_LEGACY_UNIQUE_ID = "legacy_unique_id"
VERA_ID_FORMAT = "{}_{}"

View file

@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
from . import VeraDevice
from .const import DOMAIN
from .common import ControllerData, get_controller_data
_LOGGER = logging.getLogger(__name__)
@ -24,10 +24,10 @@ async def async_setup_entry(
async_add_entities: Callable[[List[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
controller_data = hass.data[DOMAIN]
controller_data = get_controller_data(hass, entry)
async_add_entities(
[
VeraCover(device, controller_data.controller)
VeraCover(device, controller_data)
for device in controller_data.devices.get(PLATFORM_DOMAIN)
]
)
@ -36,9 +36,9 @@ async def async_setup_entry(
class VeraCover(VeraDevice, CoverEntity):
"""Representation a Vera Cover."""
def __init__(self, vera_device, controller):
def __init__(self, vera_device, controller_data: ControllerData):
"""Initialize the Vera device."""
VeraDevice.__init__(self, vera_device, controller)
VeraDevice.__init__(self, vera_device, controller_data)
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
@property

View file

@ -17,7 +17,7 @@ from homeassistant.helpers.entity import Entity
import homeassistant.util.color as color_util
from . import VeraDevice
from .const import DOMAIN
from .common import ControllerData, get_controller_data
_LOGGER = logging.getLogger(__name__)
@ -28,10 +28,10 @@ async def async_setup_entry(
async_add_entities: Callable[[List[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
controller_data = hass.data[DOMAIN]
controller_data = get_controller_data(hass, entry)
async_add_entities(
[
VeraLight(device, controller_data.controller)
VeraLight(device, controller_data)
for device in controller_data.devices.get(PLATFORM_DOMAIN)
]
)
@ -40,12 +40,12 @@ async def async_setup_entry(
class VeraLight(VeraDevice, LightEntity):
"""Representation of a Vera Light, including dimmable."""
def __init__(self, vera_device, controller):
def __init__(self, vera_device, controller_data: ControllerData):
"""Initialize the light."""
self._state = False
self._color = None
self._brightness = None
VeraDevice.__init__(self, vera_device, controller)
VeraDevice.__init__(self, vera_device, controller_data)
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
@property

View file

@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
from . import VeraDevice
from .const import DOMAIN
from .common import ControllerData, get_controller_data
_LOGGER = logging.getLogger(__name__)
@ -27,10 +27,10 @@ async def async_setup_entry(
async_add_entities: Callable[[List[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
controller_data = hass.data[DOMAIN]
controller_data = get_controller_data(hass, entry)
async_add_entities(
[
VeraLock(device, controller_data.controller)
VeraLock(device, controller_data)
for device in controller_data.devices.get(PLATFORM_DOMAIN)
]
)
@ -39,10 +39,10 @@ async def async_setup_entry(
class VeraLock(VeraDevice, LockEntity):
"""Representation of a Vera lock."""
def __init__(self, vera_device, controller):
def __init__(self, vera_device, controller_data: ControllerData):
"""Initialize the Vera device."""
self._state = None
VeraDevice.__init__(self, vera_device, controller)
VeraDevice.__init__(self, vera_device, controller_data)
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
def lock(self, **kwargs):

View file

@ -8,7 +8,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
from homeassistant.util import slugify
from .const import DOMAIN, VERA_ID_FORMAT
from .common import ControllerData, get_controller_data
from .const import VERA_ID_FORMAT
_LOGGER = logging.getLogger(__name__)
@ -19,22 +20,19 @@ async def async_setup_entry(
async_add_entities: Callable[[List[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
controller_data = hass.data[DOMAIN]
controller_data = get_controller_data(hass, entry)
async_add_entities(
[
VeraScene(device, controller_data.controller)
for device in controller_data.scenes
]
[VeraScene(device, controller_data) for device in controller_data.scenes]
)
class VeraScene(Scene):
"""Representation of a Vera scene entity."""
def __init__(self, vera_scene, controller):
def __init__(self, vera_scene, controller_data: ControllerData):
"""Initialize the scene."""
self.vera_scene = vera_scene
self.controller = controller
self.controller = controller_data.controller
self._name = self.vera_scene.name
# Append device id to prevent name clashes in HA.

View file

@ -13,7 +13,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.util import convert
from . import VeraDevice
from .const import DOMAIN
from .common import ControllerData, get_controller_data
_LOGGER = logging.getLogger(__name__)
@ -26,10 +26,10 @@ async def async_setup_entry(
async_add_entities: Callable[[List[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
controller_data = hass.data[DOMAIN]
controller_data = get_controller_data(hass, entry)
async_add_entities(
[
VeraSensor(device, controller_data.controller)
VeraSensor(device, controller_data)
for device in controller_data.devices.get(PLATFORM_DOMAIN)
]
)
@ -38,12 +38,12 @@ async def async_setup_entry(
class VeraSensor(VeraDevice, Entity):
"""Representation of a Vera Sensor."""
def __init__(self, vera_device, controller):
def __init__(self, vera_device, controller_data: ControllerData):
"""Initialize the sensor."""
self.current_value = None
self._temperature_units = None
self.last_changed_time = None
VeraDevice.__init__(self, vera_device, controller)
VeraDevice.__init__(self, vera_device, controller_data)
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
@property

View file

@ -1,7 +1,6 @@
{
"config": {
"abort": {
"already_configured": "A controller is already configured.",
"cannot_connect": "Could not connect to controller with url {base_url}"
},
"step": {

View file

@ -13,7 +13,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.util import convert
from . import VeraDevice
from .const import DOMAIN
from .common import ControllerData, get_controller_data
_LOGGER = logging.getLogger(__name__)
@ -24,10 +24,10 @@ async def async_setup_entry(
async_add_entities: Callable[[List[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
controller_data = hass.data[DOMAIN]
controller_data = get_controller_data(hass, entry)
async_add_entities(
[
VeraSwitch(device, controller_data.controller)
VeraSwitch(device, controller_data)
for device in controller_data.devices.get(PLATFORM_DOMAIN)
]
)
@ -36,10 +36,10 @@ async def async_setup_entry(
class VeraSwitch(VeraDevice, SwitchEntity):
"""Representation of a Vera Switch."""
def __init__(self, vera_device, controller):
def __init__(self, vera_device, controller_data: ControllerData):
"""Initialize the Vera device."""
self._state = False
VeraDevice.__init__(self, vera_device, controller)
VeraDevice.__init__(self, vera_device, controller_data)
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
def turn_on(self, **kwargs):

View file

@ -1,10 +1,15 @@
"""Common code for tests."""
from enum import Enum
from typing import Callable, Dict, NamedTuple, Tuple
import pyvera as pv
from homeassistant.components.vera.const import CONF_CONTROLLER, DOMAIN
from homeassistant import config_entries
from homeassistant.components.vera.const import (
CONF_CONTROLLER,
CONF_LEGACY_UNIQUE_ID,
DOMAIN,
)
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
@ -24,7 +29,15 @@ class ControllerData(NamedTuple):
class ComponentData(NamedTuple):
"""Test data about the vera component."""
controller_data: ControllerData
controller_data: Tuple[ControllerData]
class ConfigSource(Enum):
"""Source of configuration."""
FILE = "file"
CONFIG_FLOW = "config_flow"
CONFIG_ENTRY = "config_entry"
class ControllerConfig(NamedTuple):
@ -32,31 +45,34 @@ class ControllerConfig(NamedTuple):
config: Dict
options: Dict
config_from_file: bool
config_source: ConfigSource
serial_number: str
devices: Tuple[pv.VeraDevice, ...]
scenes: Tuple[pv.VeraScene, ...]
setup_callback: SetupCallback
legacy_entity_unique_id: bool
def new_simple_controller_config(
config: dict = None,
options: dict = None,
config_from_file=False,
config_source=ConfigSource.CONFIG_FLOW,
serial_number="1111",
devices: Tuple[pv.VeraDevice, ...] = (),
scenes: Tuple[pv.VeraScene, ...] = (),
setup_callback: SetupCallback = None,
legacy_entity_unique_id=False,
) -> ControllerConfig:
"""Create simple contorller config."""
return ControllerConfig(
config=config or {CONF_CONTROLLER: "http://127.0.0.1:123"},
options=options,
config_from_file=config_from_file,
config_source=config_source,
serial_number=serial_number,
devices=devices,
scenes=scenes,
setup_callback=setup_callback,
legacy_entity_unique_id=legacy_entity_unique_id,
)
@ -68,14 +84,38 @@ class ComponentFactory:
self.vera_controller_class_mock = vera_controller_class_mock
async def configure_component(
self, hass: HomeAssistant, controller_config: ControllerConfig
self,
hass: HomeAssistant,
controller_config: ControllerConfig = None,
controller_configs: Tuple[ControllerConfig] = (),
) -> ComponentData:
"""Configure the component with multiple specific mock data."""
configs = list(controller_configs)
if controller_config:
configs.append(controller_config)
return ComponentData(
controller_data=tuple(
[
await self._configure_component(hass, controller_config)
for controller_config in configs
]
)
)
async def _configure_component(
self, hass: HomeAssistant, controller_config: ControllerConfig
) -> ControllerData:
"""Configure the component with specific mock data."""
component_config = {
**(controller_config.config or {}),
**(controller_config.options or {}),
}
if controller_config.legacy_entity_unique_id:
component_config[CONF_LEGACY_UNIQUE_ID] = True
controller = MagicMock(spec=pv.VeraController) # type: pv.VeraController
controller.base_url = component_config.get(CONF_CONTROLLER)
controller.register = MagicMock()
@ -101,7 +141,7 @@ class ComponentFactory:
hass_config = {}
# Setup component through config file import.
if controller_config.config_from_file:
if controller_config.config_source == ConfigSource.FILE:
hass_config[DOMAIN] = component_config
# Setup Home Assistant.
@ -109,9 +149,21 @@ class ComponentFactory:
await hass.async_block_till_done()
# Setup component through config flow.
if not controller_config.config_from_file:
if controller_config.config_source == ConfigSource.CONFIG_FLOW:
await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
data=component_config,
)
await hass.async_block_till_done()
# Setup component directly from config entry.
if controller_config.config_source == ConfigSource.CONFIG_ENTRY:
entry = MockConfigEntry(
domain=DOMAIN, data=component_config, options={}, unique_id="12345"
domain=DOMAIN,
data=controller_config.config,
options=controller_config.options,
unique_id="12345",
)
entry.add_to_hass(hass)
@ -124,8 +176,4 @@ class ComponentFactory:
else None
)
return ComponentData(
controller_data=ControllerData(
controller=controller, update_callback=update_callback
)
)
return ControllerData(controller=controller, update_callback=update_callback)

View file

@ -14,7 +14,7 @@ async def test_binary_sensor(
"""Test function."""
vera_device = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor
vera_device.device_id = 1
vera_device.vera_device_id = 1
vera_device.vera_device_id = vera_device.device_id
vera_device.name = "dev1"
vera_device.is_tripped = False
entity_id = "binary_sensor.dev1_1"
@ -23,7 +23,7 @@ async def test_binary_sensor(
hass=hass,
controller_config=new_simple_controller_config(devices=(vera_device,)),
)
update_callback = component_data.controller_data.update_callback
update_callback = component_data.controller_data[0].update_callback
vera_device.is_tripped = False
update_callback(vera_device)

View file

@ -22,6 +22,7 @@ async def test_climate(
"""Test function."""
vera_device = MagicMock(spec=pv.VeraThermostat) # type: pv.VeraThermostat
vera_device.device_id = 1
vera_device.vera_device_id = vera_device.device_id
vera_device.name = "dev1"
vera_device.category = pv.CATEGORY_THERMOSTAT
vera_device.power = 10
@ -34,7 +35,7 @@ async def test_climate(
hass=hass,
controller_config=new_simple_controller_config(devices=(vera_device,)),
)
update_callback = component_data.controller_data.update_callback
update_callback = component_data.controller_data[0].update_callback
assert hass.states.get(entity_id).state == HVAC_MODE_OFF
@ -131,6 +132,7 @@ async def test_climate_f(
"""Test function."""
vera_device = MagicMock(spec=pv.VeraThermostat) # type: pv.VeraThermostat
vera_device.device_id = 1
vera_device.vera_device_id = vera_device.device_id
vera_device.name = "dev1"
vera_device.category = pv.CATEGORY_THERMOSTAT
vera_device.power = 10
@ -148,7 +150,7 @@ async def test_climate_f(
devices=(vera_device,), setup_callback=setup_callback
),
)
update_callback = component_data.controller_data.update_callback
update_callback = component_data.controller_data[0].update_callback
await hass.services.async_call(
"climate",

View file

@ -2,17 +2,13 @@
from requests.exceptions import RequestException
from homeassistant import config_entries, data_entry_flow
from homeassistant.components.vera import CONF_CONTROLLER, DOMAIN
from homeassistant.components.vera import CONF_CONTROLLER, CONF_LEGACY_UNIQUE_ID, DOMAIN
from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
RESULT_TYPE_CREATE_ENTRY,
RESULT_TYPE_FORM,
)
from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM
from tests.async_mock import MagicMock, patch
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, mock_registry
async def test_async_step_user_success(hass: HomeAssistant) -> None:
@ -44,6 +40,7 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None:
CONF_SOURCE: config_entries.SOURCE_USER,
CONF_LIGHTS: [12, 13],
CONF_EXCLUDE: [14, 15],
CONF_LEGACY_UNIQUE_ID: False,
}
assert result["result"].unique_id == controller.serial_number
@ -51,18 +48,6 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None:
assert entries
async def test_async_step_user_already_configured(hass: HomeAssistant) -> None:
"""Test user step with entry already configured."""
entry = MockConfigEntry(domain=DOMAIN, data={}, options={}, unique_id="12345")
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
async def test_async_step_import_success(hass: HomeAssistant) -> None:
"""Test import step success."""
with patch("pyvera.VeraController") as vera_controller_class_mock:
@ -82,28 +67,40 @@ async def test_async_step_import_success(hass: HomeAssistant) -> None:
assert result["data"] == {
CONF_CONTROLLER: "http://127.0.0.1:123",
CONF_SOURCE: config_entries.SOURCE_IMPORT,
CONF_LEGACY_UNIQUE_ID: False,
}
assert result["result"].unique_id == controller.serial_number
async def test_async_step_import_alredy_setup(hass: HomeAssistant) -> None:
"""Test import step with entry already setup."""
entry = MockConfigEntry(domain=DOMAIN, data={}, options={}, unique_id="12345")
entry.add_to_hass(hass)
async def test_async_step_import_success_with_legacy_unique_id(
hass: HomeAssistant,
) -> None:
"""Test import step success with legacy unique id."""
entity_registry = mock_registry(hass)
entity_registry.async_get_or_create(
domain="switch", platform=DOMAIN, unique_id="12"
)
with patch("pyvera.VeraController") as vera_controller_class_mock:
controller = MagicMock()
controller.refresh_data = MagicMock()
controller.serial_number = "12345"
controller.serial_number = "serial_number_1"
vera_controller_class_mock.return_value = controller
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={CONF_CONTROLLER: "http://localhost:445"},
data={CONF_CONTROLLER: "http://127.0.0.1:123/"},
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "http://127.0.0.1:123"
assert result["data"] == {
CONF_CONTROLLER: "http://127.0.0.1:123",
CONF_SOURCE: config_entries.SOURCE_IMPORT,
CONF_LEGACY_UNIQUE_ID: True,
}
assert result["result"].unique_id == controller.serial_number
async def test_async_step_finish_error(hass: HomeAssistant) -> None:

View file

@ -14,6 +14,7 @@ async def test_cover(
"""Test function."""
vera_device = MagicMock(spec=pv.VeraCurtain) # type: pv.VeraCurtain
vera_device.device_id = 1
vera_device.vera_device_id = vera_device.device_id
vera_device.name = "dev1"
vera_device.category = pv.CATEGORY_CURTAIN
vera_device.is_closed = False
@ -24,7 +25,7 @@ async def test_cover(
hass=hass,
controller_config=new_simple_controller_config(devices=(vera_device,)),
)
update_callback = component_data.controller_data.update_callback
update_callback = component_data.controller_data[0].update_callback
assert hass.states.get(entity_id).state == "closed"
assert hass.states.get(entity_id).attributes["current_position"] == 0

View file

@ -12,10 +12,10 @@ from homeassistant.components.vera import (
from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED
from homeassistant.core import HomeAssistant
from .common import ComponentFactory, new_simple_controller_config
from .common import ComponentFactory, ConfigSource, new_simple_controller_config
from tests.async_mock import MagicMock
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, mock_registry
async def test_init(
@ -24,7 +24,7 @@ async def test_init(
"""Test function."""
vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor
vera_device1.device_id = 1
vera_device1.vera_device_id = 1
vera_device1.vera_device_id = vera_device1.device_id
vera_device1.name = "first_dev"
vera_device1.is_tripped = False
entity1_id = "binary_sensor.first_dev_1"
@ -33,7 +33,7 @@ async def test_init(
hass=hass,
controller_config=new_simple_controller_config(
config={CONF_CONTROLLER: "http://127.0.0.1:111"},
config_from_file=False,
config_source=ConfigSource.CONFIG_FLOW,
serial_number="first_serial",
devices=(vera_device1,),
),
@ -41,8 +41,8 @@ async def test_init(
entity_registry = await hass.helpers.entity_registry.async_get_registry()
entry1 = entity_registry.async_get(entity1_id)
assert entry1
assert entry1.unique_id == "vera_first_serial_1"
async def test_init_from_file(
@ -51,7 +51,7 @@ async def test_init_from_file(
"""Test function."""
vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor
vera_device1.device_id = 1
vera_device1.vera_device_id = 1
vera_device1.vera_device_id = vera_device1.device_id
vera_device1.name = "first_dev"
vera_device1.is_tripped = False
entity1_id = "binary_sensor.first_dev_1"
@ -60,7 +60,7 @@ async def test_init_from_file(
hass=hass,
controller_config=new_simple_controller_config(
config={CONF_CONTROLLER: "http://127.0.0.1:111"},
config_from_file=True,
config_source=ConfigSource.FILE,
serial_number="first_serial",
devices=(vera_device1,),
),
@ -69,6 +69,62 @@ async def test_init_from_file(
entity_registry = await hass.helpers.entity_registry.async_get_registry()
entry1 = entity_registry.async_get(entity1_id)
assert entry1
assert entry1.unique_id == "vera_first_serial_1"
async def test_multiple_controllers_with_legacy_one(
hass: HomeAssistant, vera_component_factory: ComponentFactory
) -> None:
"""Test multiple controllers with one legacy controller."""
vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor
vera_device1.device_id = 1
vera_device1.vera_device_id = vera_device1.device_id
vera_device1.name = "first_dev"
vera_device1.is_tripped = False
entity1_id = "binary_sensor.first_dev_1"
vera_device2 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor
vera_device2.device_id = 2
vera_device2.vera_device_id = vera_device2.device_id
vera_device2.name = "second_dev"
vera_device2.is_tripped = False
entity2_id = "binary_sensor.second_dev_2"
# Add existing entity registry entry from previous setup.
entity_registry = mock_registry(hass)
entity_registry.async_get_or_create(
domain="switch", platform=DOMAIN, unique_id="12"
)
await vera_component_factory.configure_component(
hass=hass,
controller_config=new_simple_controller_config(
config={CONF_CONTROLLER: "http://127.0.0.1:111"},
config_source=ConfigSource.FILE,
serial_number="first_serial",
devices=(vera_device1,),
),
)
await vera_component_factory.configure_component(
hass=hass,
controller_config=new_simple_controller_config(
config={CONF_CONTROLLER: "http://127.0.0.1:222"},
config_source=ConfigSource.CONFIG_FLOW,
serial_number="second_serial",
devices=(vera_device2,),
),
)
entity_registry = await hass.helpers.entity_registry.async_get_registry()
entry1 = entity_registry.async_get(entity1_id)
assert entry1
assert entry1.unique_id == "1"
entry2 = entity_registry.async_get(entity2_id)
assert entry2
assert entry2.unique_id == "vera_second_serial_2"
async def test_unload(
@ -77,7 +133,7 @@ async def test_unload(
"""Test function."""
vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor
vera_device1.device_id = 1
vera_device1.vera_device_id = 1
vera_device1.vera_device_id = vera_device1.device_id
vera_device1.name = "first_dev"
vera_device1.is_tripped = False
@ -145,6 +201,7 @@ async def test_exclude_and_light_ids(
vera_device3 = MagicMock(spec=pv.VeraSwitch) # type: pv.VeraSwitch
vera_device3.device_id = 3
vera_device3.vera_device_id = 3
vera_device3.name = "dev3"
vera_device3.category = pv.CATEGORY_SWITCH
vera_device3.is_switched_on = MagicMock(return_value=False)
@ -152,6 +209,7 @@ async def test_exclude_and_light_ids(
vera_device4 = MagicMock(spec=pv.VeraSwitch) # type: pv.VeraSwitch
vera_device4.device_id = 4
vera_device4.vera_device_id = 4
vera_device4.name = "dev4"
vera_device4.category = pv.CATEGORY_SWITCH
vera_device4.is_switched_on = MagicMock(return_value=False)
@ -160,6 +218,7 @@ async def test_exclude_and_light_ids(
component_data = await vera_component_factory.configure_component(
hass=hass,
controller_config=new_simple_controller_config(
config_source=ConfigSource.CONFIG_ENTRY,
devices=(vera_device1, vera_device2, vera_device3, vera_device4),
config={**{CONF_CONTROLLER: "http://127.0.0.1:123"}, **options},
),
@ -167,12 +226,10 @@ async def test_exclude_and_light_ids(
# Assert the entries were setup correctly.
config_entry = next(iter(hass.config_entries.async_entries(DOMAIN)))
assert config_entry.options == {
CONF_LIGHTS: [4, 10, 12],
CONF_EXCLUDE: [1],
}
assert config_entry.options[CONF_LIGHTS] == [4, 10, 12]
assert config_entry.options[CONF_EXCLUDE] == [1]
update_callback = component_data.controller_data.update_callback
update_callback = component_data.controller_data[0].update_callback
update_callback(vera_device1)
update_callback(vera_device2)

View file

@ -15,6 +15,7 @@ async def test_light(
"""Test function."""
vera_device = MagicMock(spec=pv.VeraDimmer) # type: pv.VeraDimmer
vera_device.device_id = 1
vera_device.vera_device_id = vera_device.device_id
vera_device.name = "dev1"
vera_device.category = pv.CATEGORY_DIMMER
vera_device.is_switched_on = MagicMock(return_value=False)
@ -27,7 +28,7 @@ async def test_light(
hass=hass,
controller_config=new_simple_controller_config(devices=(vera_device,)),
)
update_callback = component_data.controller_data.update_callback
update_callback = component_data.controller_data[0].update_callback
assert hass.states.get(entity_id).state == "off"

View file

@ -15,6 +15,7 @@ async def test_lock(
"""Test function."""
vera_device = MagicMock(spec=pv.VeraLock) # type: pv.VeraLock
vera_device.device_id = 1
vera_device.vera_device_id = vera_device.device_id
vera_device.name = "dev1"
vera_device.category = pv.CATEGORY_LOCK
vera_device.is_locked = MagicMock(return_value=False)
@ -24,7 +25,7 @@ async def test_lock(
hass=hass,
controller_config=new_simple_controller_config(devices=(vera_device,)),
)
update_callback = component_data.controller_data.update_callback
update_callback = component_data.controller_data[0].update_callback
assert hass.states.get(entity_id).state == STATE_UNLOCKED

View file

@ -14,6 +14,7 @@ async def test_scene(
"""Test function."""
vera_scene = MagicMock(spec=pv.VeraScene) # type: pv.VeraScene
vera_scene.scene_id = 1
vera_scene.vera_scene_id = vera_scene.scene_id
vera_scene.name = "dev1"
entity_id = "scene.dev1_1"

View file

@ -23,6 +23,7 @@ async def run_sensor_test(
"""Test generic sensor."""
vera_device = MagicMock(spec=pv.VeraSensor) # type: pv.VeraSensor
vera_device.device_id = 1
vera_device.vera_device_id = vera_device.device_id
vera_device.name = "dev1"
vera_device.category = category
setattr(vera_device, class_property, "33")
@ -34,7 +35,7 @@ async def run_sensor_test(
devices=(vera_device,), setup_callback=setup_callback
),
)
update_callback = component_data.controller_data.update_callback
update_callback = component_data.controller_data[0].update_callback
for (initial_value, state_value) in assert_states:
setattr(vera_device, class_property, initial_value)
@ -175,6 +176,7 @@ async def test_scene_controller_sensor(
"""Test function."""
vera_device = MagicMock(spec=pv.VeraSensor) # type: pv.VeraSensor
vera_device.device_id = 1
vera_device.vera_device_id = vera_device.device_id
vera_device.name = "dev1"
vera_device.category = pv.CATEGORY_SCENE_CONTROLLER
vera_device.get_last_scene_id = MagicMock(return_value="id0")
@ -185,7 +187,7 @@ async def test_scene_controller_sensor(
hass=hass,
controller_config=new_simple_controller_config(devices=(vera_device,)),
)
update_callback = component_data.controller_data.update_callback
update_callback = component_data.controller_data[0].update_callback
vera_device.get_last_scene_time.return_value = "1111"
update_callback(vera_device)

View file

@ -14,6 +14,7 @@ async def test_switch(
"""Test function."""
vera_device = MagicMock(spec=pv.VeraSwitch) # type: pv.VeraSwitch
vera_device.device_id = 1
vera_device.vera_device_id = vera_device.device_id
vera_device.name = "dev1"
vera_device.category = pv.CATEGORY_SWITCH
vera_device.is_switched_on = MagicMock(return_value=False)
@ -21,9 +22,11 @@ async def test_switch(
component_data = await vera_component_factory.configure_component(
hass=hass,
controller_config=new_simple_controller_config(devices=(vera_device,)),
controller_config=new_simple_controller_config(
devices=(vera_device,), legacy_entity_unique_id=False
),
)
update_callback = component_data.controller_data.update_callback
update_callback = component_data.controller_data[0].update_callback
assert hass.states.get(entity_id).state == "off"