New configuration flow for ZHA integration (#35161)

* Start gateway using new zigpy init.

Update config entry data  import.
Use new zigpy startup.
Fix config entry import without zha config section.
Auto form Zigbee network.

* Migrate config entry.

* New ZHA config entry flow.

Use lightweight probe() method for ZHA config entry validation when
available. Failback to old behavior of setting up Zigpy app if radio lib
does not provide probing.

* Clean ZHA_GW_RADIO

* Don't import ZHA device settings.

* Update config flow tests.

* Filter out empty manufacturer.

* Replace port path with an by-id device name.

* Rebase cleanup

* Use correct mock.

* Make lint happy again

* Update tests/components/zha/test_config_flow.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update tests/components/zha/test_config_flow.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update tests/components/zha/test_config_flow.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Use executor pool for IO

* Address comments.

Use AsyncMock from tests.

* Use core interface to test config flow.

* Use core interface to test config_flow.

* Address comments. Use core interface.

* Update ZHA dependencies.

* Schema guard

* Use async_update_entry for migration.

* Don't allow schema extra keys.

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Alexei Chetroi 2020-05-06 06:23:53 -04:00 committed by GitHub
parent 2581b031d9
commit c71a7b901f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 527 additions and 249 deletions

View file

@ -860,7 +860,6 @@ omit =
homeassistant/components/zengge/light.py homeassistant/components/zengge/light.py
homeassistant/components/zeroconf/* homeassistant/components/zeroconf/*
homeassistant/components/zestimate/sensor.py homeassistant/components/zestimate/sensor.py
homeassistant/components/zha/__init__.py
homeassistant/components/zha/api.py homeassistant/components/zha/api.py
homeassistant/components/zha/core/channels/* homeassistant/components/zha/core/channels/*
homeassistant/components/zha/core/const.py homeassistant/components/zha/core/const.py

View file

@ -4,6 +4,7 @@ import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH
from homeassistant import config_entries, const as ha_const from homeassistant import config_entries, const as ha_const
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -14,6 +15,7 @@ from homeassistant.helpers.typing import HomeAssistantType
from . import api from . import api
from .core import ZHAGateway from .core import ZHAGateway
from .core.const import ( from .core.const import (
BAUD_RATES,
COMPONENTS, COMPONENTS,
CONF_BAUDRATE, CONF_BAUDRATE,
CONF_DATABASE, CONF_DATABASE,
@ -21,13 +23,12 @@ from .core.const import (
CONF_ENABLE_QUIRKS, CONF_ENABLE_QUIRKS,
CONF_RADIO_TYPE, CONF_RADIO_TYPE,
CONF_USB_PATH, CONF_USB_PATH,
CONF_ZIGPY,
DATA_ZHA, DATA_ZHA,
DATA_ZHA_CONFIG, DATA_ZHA_CONFIG,
DATA_ZHA_DISPATCHERS, DATA_ZHA_DISPATCHERS,
DATA_ZHA_GATEWAY, DATA_ZHA_GATEWAY,
DATA_ZHA_PLATFORM_LOADED, DATA_ZHA_PLATFORM_LOADED,
DEFAULT_BAUDRATE,
DEFAULT_RADIO_TYPE,
DOMAIN, DOMAIN,
SIGNAL_ADD_ENTITIES, SIGNAL_ADD_ENTITIES,
RadioType, RadioType,
@ -35,23 +36,25 @@ from .core.const import (
from .core.discovery import GROUP_PROBE from .core.discovery import GROUP_PROBE
DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({vol.Optional(ha_const.CONF_TYPE): cv.string}) DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({vol.Optional(ha_const.CONF_TYPE): cv.string})
ZHA_CONFIG_SCHEMA = {
vol.Optional(CONF_BAUDRATE): cv.positive_int,
vol.Optional(CONF_DATABASE): cv.string,
vol.Optional(CONF_DEVICE_CONFIG, default={}): vol.Schema(
{cv.string: DEVICE_CONFIG_SCHEMA_ENTRY}
),
vol.Optional(CONF_ENABLE_QUIRKS, default=True): cv.boolean,
vol.Optional(CONF_ZIGPY): dict,
}
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: vol.Schema( DOMAIN: vol.Schema(
{ vol.All(
vol.Optional(CONF_RADIO_TYPE, default=DEFAULT_RADIO_TYPE): cv.enum( cv.deprecated(CONF_USB_PATH, invalidation_version="0.112"),
RadioType cv.deprecated(CONF_BAUDRATE, invalidation_version="0.112"),
), cv.deprecated(CONF_RADIO_TYPE, invalidation_version="0.112"),
CONF_USB_PATH: cv.string, ZHA_CONFIG_SCHEMA,
vol.Optional(CONF_BAUDRATE, default=DEFAULT_BAUDRATE): cv.positive_int, ),
vol.Optional(CONF_DATABASE): cv.string, ),
vol.Optional(CONF_DEVICE_CONFIG, default={}): vol.Schema(
{cv.string: DEVICE_CONFIG_SCHEMA_ENTRY}
),
vol.Optional(CONF_ENABLE_QUIRKS, default=True): cv.boolean,
}
)
}, },
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
@ -67,23 +70,10 @@ async def async_setup(hass, config):
"""Set up ZHA from config.""" """Set up ZHA from config."""
hass.data[DATA_ZHA] = {} hass.data[DATA_ZHA] = {}
if DOMAIN not in config: if DOMAIN in config:
return True conf = config[DOMAIN]
hass.data[DATA_ZHA][DATA_ZHA_CONFIG] = conf
conf = config[DOMAIN]
hass.data[DATA_ZHA][DATA_ZHA_CONFIG] = conf
if not hass.config_entries.async_entries(DOMAIN):
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={
CONF_USB_PATH: conf[CONF_USB_PATH],
CONF_RADIO_TYPE: conf.get(CONF_RADIO_TYPE).value,
},
)
)
return True return True
@ -161,3 +151,26 @@ async def async_load_entities(hass: HomeAssistantType) -> None:
if isinstance(res, Exception): if isinstance(res, Exception):
_LOGGER.warning("Couldn't setup zha platform: %s", res) _LOGGER.warning("Couldn't setup zha platform: %s", res)
async_dispatcher_send(hass, SIGNAL_ADD_ENTITIES) async_dispatcher_send(hass, SIGNAL_ADD_ENTITIES)
async def async_migrate_entry(
hass: HomeAssistantType, config_entry: config_entries.ConfigEntry
):
"""Migrate old entry."""
_LOGGER.debug("Migrating from version %s", config_entry.version)
if config_entry.version == 1:
data = {
CONF_RADIO_TYPE: config_entry.data[CONF_RADIO_TYPE],
CONF_DEVICE: {CONF_DEVICE_PATH: config_entry.data[CONF_USB_PATH]},
}
baudrate = hass.data[DATA_ZHA].get(DATA_ZHA_CONFIG, {}).get(CONF_BAUDRATE)
if data[CONF_RADIO_TYPE] != RadioType.deconz and baudrate in BAUD_RATES:
data[CONF_DEVICE][CONF_BAUDRATE] = baudrate
config_entry.version = 2
hass.config_entries.async_update_entry(config_entry, data=data)
_LOGGER.info("Migration to version %s successful", config_entry.version)
return True

View file

@ -1,80 +1,142 @@
"""Config flow for ZHA.""" """Config flow for ZHA."""
import asyncio
from collections import OrderedDict
import os import os
from typing import Any, Dict, Optional
import serial.tools.list_ports
import voluptuous as vol import voluptuous as vol
from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH
from homeassistant import config_entries from homeassistant import config_entries
from .core.const import ( from .core.const import ( # pylint:disable=unused-import
CONF_BAUDRATE,
CONF_FLOWCONTROL,
CONF_RADIO_TYPE, CONF_RADIO_TYPE,
CONF_USB_PATH,
CONTROLLER, CONTROLLER,
DEFAULT_BAUDRATE,
DEFAULT_DATABASE_NAME,
DOMAIN, DOMAIN,
ZHA_GW_RADIO,
RadioType, RadioType,
) )
from .core.registries import RADIO_TYPES from .core.registries import RADIO_TYPES
CONF_MANUAL_PATH = "Enter Manually"
SUPPORTED_PORT_SETTINGS = (
CONF_BAUDRATE,
CONF_FLOWCONTROL,
)
@config_entries.HANDLERS.register(DOMAIN)
class ZhaFlowHandler(config_entries.ConfigFlow): class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow.""" """Handle a config flow."""
VERSION = 1 VERSION = 2
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
def __init__(self):
"""Initialize flow instance."""
self._device_path = None
self._radio_type = None
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):
"""Handle a zha config flow start.""" """Handle a zha config flow start."""
if self._async_current_entries(): if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed") return self.async_abort(reason="single_instance_allowed")
errors = {} ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports)
list_of_ports = [
fields = OrderedDict() f"{p}, s/n: {p.serial_number or 'n/a'}"
fields[vol.Required(CONF_USB_PATH)] = str + (f" - {p.manufacturer}" if p.manufacturer else "")
fields[vol.Optional(CONF_RADIO_TYPE, default="ezsp")] = vol.In(RadioType.list()) for p in ports
]
list_of_ports.append(CONF_MANUAL_PATH)
if user_input is not None: if user_input is not None:
database = os.path.join(self.hass.config.config_dir, DEFAULT_DATABASE_NAME) user_selection = user_input[CONF_DEVICE_PATH]
test = await check_zigpy_connection( if user_selection == CONF_MANUAL_PATH:
user_input[CONF_USB_PATH], user_input[CONF_RADIO_TYPE], database return await self.async_step_pick_radio()
port = ports[list_of_ports.index(user_selection)]
dev_path = await self.hass.async_add_executor_job(
get_serial_by_id, port.device
) )
if test: auto_detected_data = await detect_radios(dev_path)
if auto_detected_data is not None:
title = f"{port.description}, s/n: {port.serial_number or 'n/a'}"
title += f" - {port.manufacturer}" if port.manufacturer else ""
return self.async_create_entry(title=title, data=auto_detected_data,)
# did not detect anything
self._device_path = dev_path
return await self.async_step_pick_radio()
schema = vol.Schema({vol.Required(CONF_DEVICE_PATH): vol.In(list_of_ports)})
return self.async_show_form(step_id="user", data_schema=schema)
async def async_step_pick_radio(self, user_input=None):
"""Select radio type."""
if user_input is not None:
self._radio_type = user_input[CONF_RADIO_TYPE]
return await self.async_step_port_config()
schema = {vol.Required(CONF_RADIO_TYPE): vol.In(sorted(RadioType.list()))}
return self.async_show_form(
step_id="pick_radio", data_schema=vol.Schema(schema),
)
async def async_step_port_config(self, user_input=None):
"""Enter port settings specific for this type of radio."""
errors = {}
app_cls = RADIO_TYPES[self._radio_type][CONTROLLER]
if user_input is not None:
self._device_path = user_input.get(CONF_DEVICE_PATH)
if await app_cls.probe(user_input):
serial_by_id = await self.hass.async_add_executor_job(
get_serial_by_id, user_input[CONF_DEVICE_PATH]
)
user_input[CONF_DEVICE_PATH] = serial_by_id
return self.async_create_entry( return self.async_create_entry(
title=user_input[CONF_USB_PATH], data=user_input title=user_input[CONF_DEVICE_PATH],
data={CONF_DEVICE: user_input, CONF_RADIO_TYPE: self._radio_type},
) )
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
schema = {
vol.Required(
CONF_DEVICE_PATH, default=self._device_path or vol.UNDEFINED
): str
}
radio_schema = app_cls.SCHEMA_DEVICE.schema
if isinstance(radio_schema, vol.Schema):
radio_schema = radio_schema.schema
for param, value in radio_schema.items():
if param in SUPPORTED_PORT_SETTINGS:
schema[param] = value
return self.async_show_form( return self.async_show_form(
step_id="user", data_schema=vol.Schema(fields), errors=errors step_id="port_config", data_schema=vol.Schema(schema), errors=errors,
)
async def async_step_import(self, import_info):
"""Handle a zha config import."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
return self.async_create_entry(
title=import_info[CONF_USB_PATH], data=import_info
) )
async def check_zigpy_connection(usb_path, radio_type, database_path): async def detect_radios(dev_path: str) -> Optional[Dict[str, Any]]:
"""Test zigpy radio connection.""" """Probe all radio types on the device port."""
try: for radio in RadioType.list():
radio = RADIO_TYPES[radio_type][ZHA_GW_RADIO]() app_cls = RADIO_TYPES[radio][CONTROLLER]
controller_application = RADIO_TYPES[radio_type][CONTROLLER] dev_config = app_cls.SCHEMA_DEVICE({CONF_DEVICE_PATH: dev_path})
except KeyError: if await app_cls.probe(dev_config):
return False return {CONF_RADIO_TYPE: radio, CONF_DEVICE: dev_config}
try:
await radio.connect(usb_path, DEFAULT_BAUDRATE) return None
controller = controller_application(radio, database_path)
await asyncio.wait_for(controller.startup(auto_form=True), timeout=30)
await controller.shutdown() def get_serial_by_id(dev_path: str) -> str:
except Exception: # pylint: disable=broad-except """Return a /dev/serial/by-id match for given device if available."""
return False by_id = "/dev/serial/by-id"
return True if not os.path.isdir(by_id):
return dev_path
for path in (entry.path for entry in os.scandir(by_id) if entry.is_symlink()):
if os.path.realpath(path) == dev_path:
return path
return dev_path

View file

@ -2,6 +2,8 @@
import enum import enum
import logging import logging
from zigpy.config import CONF_DEVICE_PATH # noqa: F401 # pylint: disable=unused-import
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR
from homeassistant.components.cover import DOMAIN as COVER from homeassistant.components.cover import DOMAIN as COVER
from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER
@ -92,8 +94,10 @@ CONF_BAUDRATE = "baudrate"
CONF_DATABASE = "database_path" CONF_DATABASE = "database_path"
CONF_DEVICE_CONFIG = "device_config" CONF_DEVICE_CONFIG = "device_config"
CONF_ENABLE_QUIRKS = "enable_quirks" CONF_ENABLE_QUIRKS = "enable_quirks"
CONF_FLOWCONTROL = "flow_control"
CONF_RADIO_TYPE = "radio_type" CONF_RADIO_TYPE = "radio_type"
CONF_USB_PATH = "usb_path" CONF_USB_PATH = "usb_path"
CONF_ZIGPY = "zigpy_config"
CONTROLLER = "controller" CONTROLLER = "controller"
DATA_DEVICE_CONFIG = "zha_device_config" DATA_DEVICE_CONFIG = "zha_device_config"
@ -145,11 +149,11 @@ POWER_BATTERY_OR_UNKNOWN = "Battery or Unknown"
class RadioType(enum.Enum): class RadioType(enum.Enum):
"""Possible options for radio type.""" """Possible options for radio type."""
deconz = "deconz"
ezsp = "ezsp" ezsp = "ezsp"
deconz = "deconz"
ti_cc = "ti_cc" ti_cc = "ti_cc"
xbee = "xbee"
zigate = "zigate" zigate = "zigate"
xbee = "xbee"
@classmethod @classmethod
def list(cls): def list(cls):
@ -258,7 +262,6 @@ ZHA_GW_MSG_GROUP_REMOVED = "group_removed"
ZHA_GW_MSG_LOG_ENTRY = "log_entry" ZHA_GW_MSG_LOG_ENTRY = "log_entry"
ZHA_GW_MSG_LOG_OUTPUT = "log_output" ZHA_GW_MSG_LOG_OUTPUT = "log_output"
ZHA_GW_MSG_RAW_INIT = "raw_device_initialized" ZHA_GW_MSG_RAW_INIT = "raw_device_initialized"
ZHA_GW_RADIO = "radio"
ZHA_GW_RADIO_DESCRIPTION = "radio_description" ZHA_GW_RADIO_DESCRIPTION = "radio_description"
EFFECT_BLINK = 0x00 EFFECT_BLINK = 0x00

View file

@ -10,6 +10,7 @@ import traceback
from typing import List, Optional from typing import List, Optional
from serial import SerialException from serial import SerialException
from zigpy.config import CONF_DEVICE
import zigpy.device as zigpy_dev import zigpy.device as zigpy_dev
from homeassistant.components.system_log import LogEntry, _figure_out_source from homeassistant.components.system_log import LogEntry, _figure_out_source
@ -33,10 +34,9 @@ from .const import (
ATTR_NWK, ATTR_NWK,
ATTR_SIGNATURE, ATTR_SIGNATURE,
ATTR_TYPE, ATTR_TYPE,
CONF_BAUDRATE,
CONF_DATABASE, CONF_DATABASE,
CONF_RADIO_TYPE, CONF_RADIO_TYPE,
CONF_USB_PATH, CONF_ZIGPY,
CONTROLLER, CONTROLLER,
DATA_ZHA, DATA_ZHA,
DATA_ZHA_BRIDGE_ID, DATA_ZHA_BRIDGE_ID,
@ -52,7 +52,6 @@ from .const import (
DEBUG_LEVEL_ORIGINAL, DEBUG_LEVEL_ORIGINAL,
DEBUG_LEVELS, DEBUG_LEVELS,
DEBUG_RELAY_LOGGERS, DEBUG_RELAY_LOGGERS,
DEFAULT_BAUDRATE,
DEFAULT_DATABASE_NAME, DEFAULT_DATABASE_NAME,
DOMAIN, DOMAIN,
SIGNAL_ADD_ENTITIES, SIGNAL_ADD_ENTITIES,
@ -74,7 +73,6 @@ from .const import (
ZHA_GW_MSG_LOG_ENTRY, ZHA_GW_MSG_LOG_ENTRY,
ZHA_GW_MSG_LOG_OUTPUT, ZHA_GW_MSG_LOG_OUTPUT,
ZHA_GW_MSG_RAW_INIT, ZHA_GW_MSG_RAW_INIT,
ZHA_GW_RADIO,
ZHA_GW_RADIO_DESCRIPTION, ZHA_GW_RADIO_DESCRIPTION,
) )
from .device import DeviceStatus, ZHADevice from .device import DeviceStatus, ZHADevice
@ -125,43 +123,35 @@ class ZHAGateway:
self.ha_device_registry = await get_dev_reg(self._hass) self.ha_device_registry = await get_dev_reg(self._hass)
self.ha_entity_registry = await get_ent_reg(self._hass) self.ha_entity_registry = await get_ent_reg(self._hass)
usb_path = self._config_entry.data.get(CONF_USB_PATH) radio_type = self._config_entry.data[CONF_RADIO_TYPE]
baudrate = self._config.get(CONF_BAUDRATE, DEFAULT_BAUDRATE)
radio_type = self._config_entry.data.get(CONF_RADIO_TYPE)
radio_details = RADIO_TYPES[radio_type] app_controller_cls = RADIO_TYPES[radio_type][CONTROLLER]
radio = radio_details[ZHA_GW_RADIO]() self.radio_description = RADIO_TYPES[radio_type][ZHA_GW_RADIO_DESCRIPTION]
self.radio_description = radio_details[ZHA_GW_RADIO_DESCRIPTION]
app_config = self._config.get(CONF_ZIGPY, {})
database = self._config.get(
CONF_DATABASE,
os.path.join(self._hass.config.config_dir, DEFAULT_DATABASE_NAME),
)
app_config[CONF_DATABASE] = database
app_config[CONF_DEVICE] = self._config_entry.data[CONF_DEVICE]
app_config = app_controller_cls.SCHEMA(app_config)
try: try:
await radio.connect(usb_path, baudrate) self.application_controller = await app_controller_cls.new(
except (SerialException, OSError) as exception: app_config, auto_form=True, start_radio=True
_LOGGER.error("Couldn't open serial port for ZHA: %s", str(exception)) )
raise ConfigEntryNotReady except (asyncio.TimeoutError, SerialException, OSError) as exception:
_LOGGER.error(
"Couldn't start %s coordinator",
self.radio_description,
exc_info=exception,
)
raise ConfigEntryNotReady from exception
if CONF_DATABASE in self._config:
database = self._config[CONF_DATABASE]
else:
database = os.path.join(self._hass.config.config_dir, DEFAULT_DATABASE_NAME)
self.application_controller = radio_details[CONTROLLER](radio, database)
apply_application_controller_patch(self) apply_application_controller_patch(self)
self.application_controller.add_listener(self) self.application_controller.add_listener(self)
self.application_controller.groups.add_listener(self) self.application_controller.groups.add_listener(self)
try:
res = await self.application_controller.startup(auto_form=True)
if res is False:
await self.application_controller.shutdown()
raise ConfigEntryNotReady
except asyncio.TimeoutError as exception:
_LOGGER.error(
"Couldn't start %s coordinator",
radio_details[ZHA_GW_RADIO_DESCRIPTION],
exc_info=exception,
)
radio.close()
raise ConfigEntryNotReady from exception
self._hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self self._hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self
self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str( self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(
self.application_controller.ieee self.application_controller.ieee

View file

@ -3,18 +3,13 @@ import collections
from typing import Callable, Dict, List, Set, Tuple, Union from typing import Callable, Dict, List, Set, Tuple, Union
import attr import attr
import bellows.ezsp
import bellows.zigbee.application import bellows.zigbee.application
import zigpy.profiles.zha import zigpy.profiles.zha
import zigpy.profiles.zll import zigpy.profiles.zll
import zigpy.zcl as zcl import zigpy.zcl as zcl
import zigpy_cc.api
import zigpy_cc.zigbee.application import zigpy_cc.zigbee.application
import zigpy_deconz.api
import zigpy_deconz.zigbee.application import zigpy_deconz.zigbee.application
import zigpy_xbee.api
import zigpy_xbee.zigbee.application import zigpy_xbee.zigbee.application
import zigpy_zigate.api
import zigpy_zigate.zigbee.application import zigpy_zigate.zigbee.application
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR
@ -28,7 +23,7 @@ from homeassistant.components.switch import DOMAIN as SWITCH
# importing channels updates registries # importing channels updates registries
from . import channels as zha_channels # noqa: F401 pylint: disable=unused-import from . import channels as zha_channels # noqa: F401 pylint: disable=unused-import
from .const import CONTROLLER, ZHA_GW_RADIO, ZHA_GW_RADIO_DESCRIPTION, RadioType from .const import CONTROLLER, ZHA_GW_RADIO_DESCRIPTION, RadioType
from .decorators import CALLABLE_T, DictRegistry, SetRegistry from .decorators import CALLABLE_T, DictRegistry, SetRegistry
from .typing import ChannelType from .typing import ChannelType
@ -131,27 +126,22 @@ CLIENT_CHANNELS_REGISTRY = DictRegistry()
RADIO_TYPES = { RADIO_TYPES = {
RadioType.deconz.name: { RadioType.deconz.name: {
ZHA_GW_RADIO: zigpy_deconz.api.Deconz,
CONTROLLER: zigpy_deconz.zigbee.application.ControllerApplication, CONTROLLER: zigpy_deconz.zigbee.application.ControllerApplication,
ZHA_GW_RADIO_DESCRIPTION: "Deconz", ZHA_GW_RADIO_DESCRIPTION: "Deconz",
}, },
RadioType.ezsp.name: { RadioType.ezsp.name: {
ZHA_GW_RADIO: bellows.ezsp.EZSP,
CONTROLLER: bellows.zigbee.application.ControllerApplication, CONTROLLER: bellows.zigbee.application.ControllerApplication,
ZHA_GW_RADIO_DESCRIPTION: "EZSP", ZHA_GW_RADIO_DESCRIPTION: "EZSP",
}, },
RadioType.ti_cc.name: { RadioType.ti_cc.name: {
ZHA_GW_RADIO: zigpy_cc.api.API,
CONTROLLER: zigpy_cc.zigbee.application.ControllerApplication, CONTROLLER: zigpy_cc.zigbee.application.ControllerApplication,
ZHA_GW_RADIO_DESCRIPTION: "TI CC", ZHA_GW_RADIO_DESCRIPTION: "TI CC",
}, },
RadioType.xbee.name: { RadioType.xbee.name: {
ZHA_GW_RADIO: zigpy_xbee.api.XBee,
CONTROLLER: zigpy_xbee.zigbee.application.ControllerApplication, CONTROLLER: zigpy_xbee.zigbee.application.ControllerApplication,
ZHA_GW_RADIO_DESCRIPTION: "XBee", ZHA_GW_RADIO_DESCRIPTION: "XBee",
}, },
RadioType.zigate.name: { RadioType.zigate.name: {
ZHA_GW_RADIO: zigpy_zigate.api.ZiGate,
CONTROLLER: zigpy_zigate.zigbee.application.ControllerApplication, CONTROLLER: zigpy_zigate.zigbee.application.ControllerApplication,
ZHA_GW_RADIO_DESCRIPTION: "ZiGate", ZHA_GW_RADIO_DESCRIPTION: "ZiGate",
}, },

View file

@ -4,13 +4,14 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zha", "documentation": "https://www.home-assistant.io/integrations/zha",
"requirements": [ "requirements": [
"bellows-homeassistant==0.15.2", "bellows==0.16.1",
"pyserial==3.4",
"zha-quirks==0.0.38", "zha-quirks==0.0.38",
"zigpy-cc==0.3.1", "zigpy-cc==0.4.2",
"zigpy-deconz==0.8.1", "zigpy-deconz==0.9.1",
"zigpy-homeassistant==0.19.0", "zigpy==0.20.1",
"zigpy-xbee-homeassistant==0.11.0", "zigpy-xbee==0.12.1",
"zigpy-zigate==0.5.1" "zigpy-zigate==0.6.1"
], ],
"codeowners": ["@dmulcahey", "@adminiuga"] "codeowners": ["@dmulcahey", "@adminiuga"]
} }

View file

@ -3,9 +3,21 @@
"step": { "step": {
"user": { "user": {
"title": "ZHA", "title": "ZHA",
"data": { "path": "Serial Device Path" },
"description": "Select serial port for Zigbee radio"
},
"pick_radio": {
"data": { "radio_type": "Radio Type" },
"title": "Radio Type",
"description": "Pick a type of your Zigbee radio"
},
"port_config": {
"title": "Settings",
"description": "Enter port specific settings",
"data": { "data": {
"radio_type": "Radio Type", "path": "Serial device path",
"usb_path": "[%key:common::config_flow::data::usb_path%]" "baudrate": "port speed",
"flow_control": "data flow control"
} }
} }
}, },

View file

@ -7,11 +7,27 @@
"cannot_connect": "Unable to connect to ZHA device." "cannot_connect": "Unable to connect to ZHA device."
}, },
"step": { "step": {
"pick_radio": {
"data": {
"radio_type": "Radio Type"
},
"description": "Pick a type of your Zigbee radio",
"title": "Radio Type"
},
"port_config": {
"data": {
"baudrate": "port speed",
"flow_control": "data flow control",
"path": "Serial device path"
},
"description": "Enter port specific settings",
"title": "Settings"
},
"user": { "user": {
"data": { "data": {
"radio_type": "Radio Type", "path": "Serial Device Path"
"usb_path": "USB Device Path"
}, },
"description": "Select serial port for Zigbee radio",
"title": "ZHA" "title": "ZHA"
} }
} }

View file

@ -327,7 +327,7 @@ beautifulsoup4==4.9.0
beewi_smartclim==0.0.7 beewi_smartclim==0.0.7
# homeassistant.components.zha # homeassistant.components.zha
bellows-homeassistant==0.15.2 bellows==0.16.1
# homeassistant.components.bmw_connected_drive # homeassistant.components.bmw_connected_drive
bimmer_connected==0.7.5 bimmer_connected==0.7.5
@ -1557,6 +1557,7 @@ pysensibo==1.0.3
pyserial-asyncio==0.4 pyserial-asyncio==0.4
# homeassistant.components.acer_projector # homeassistant.components.acer_projector
# homeassistant.components.zha
pyserial==3.4 pyserial==3.4
# homeassistant.components.sesame # homeassistant.components.sesame
@ -2217,19 +2218,19 @@ zhong_hong_hvac==1.0.9
ziggo-mediabox-xl==1.1.0 ziggo-mediabox-xl==1.1.0
# homeassistant.components.zha # homeassistant.components.zha
zigpy-cc==0.3.1 zigpy-cc==0.4.2
# homeassistant.components.zha # homeassistant.components.zha
zigpy-deconz==0.8.1 zigpy-deconz==0.9.1
# homeassistant.components.zha # homeassistant.components.zha
zigpy-homeassistant==0.19.0 zigpy-xbee==0.12.1
# homeassistant.components.zha # homeassistant.components.zha
zigpy-xbee-homeassistant==0.11.0 zigpy-zigate==0.6.1
# homeassistant.components.zha # homeassistant.components.zha
zigpy-zigate==0.5.1 zigpy==0.20.1
# homeassistant.components.zoneminder # homeassistant.components.zoneminder
zm-py==0.4.0 zm-py==0.4.0

View file

@ -141,7 +141,7 @@ axis==25
base36==0.1.1 base36==0.1.1
# homeassistant.components.zha # homeassistant.components.zha
bellows-homeassistant==0.15.2 bellows==0.16.1
# homeassistant.components.blebox # homeassistant.components.blebox
blebox_uniapi==1.3.2 blebox_uniapi==1.3.2
@ -634,6 +634,10 @@ pyps4-2ndscreen==1.0.7
# homeassistant.components.qwikswitch # homeassistant.components.qwikswitch
pyqwikswitch==0.93 pyqwikswitch==0.93
# homeassistant.components.acer_projector
# homeassistant.components.zha
pyserial==3.4
# homeassistant.components.signal_messenger # homeassistant.components.signal_messenger
pysignalclirestapi==0.3.4 pysignalclirestapi==0.3.4
@ -857,16 +861,16 @@ zeroconf==0.26.0
zha-quirks==0.0.38 zha-quirks==0.0.38
# homeassistant.components.zha # homeassistant.components.zha
zigpy-cc==0.3.1 zigpy-cc==0.4.2
# homeassistant.components.zha # homeassistant.components.zha
zigpy-deconz==0.8.1 zigpy-deconz==0.9.1
# homeassistant.components.zha # homeassistant.components.zha
zigpy-homeassistant==0.19.0 zigpy-xbee==0.12.1
# homeassistant.components.zha # homeassistant.components.zha
zigpy-xbee-homeassistant==0.11.0 zigpy-zigate==0.6.1
# homeassistant.components.zha # homeassistant.components.zha
zigpy-zigate==0.5.1 zigpy==0.20.1

View file

@ -4,6 +4,7 @@ from unittest import mock
import pytest import pytest
import zigpy import zigpy
from zigpy.application import ControllerApplication from zigpy.application import ControllerApplication
import zigpy.config
import zigpy.group import zigpy.group
import zigpy.types import zigpy.types
@ -49,12 +50,11 @@ def zigpy_radio():
async def config_entry_fixture(hass): async def config_entry_fixture(hass):
"""Fixture representing a config entry.""" """Fixture representing a config entry."""
entry = MockConfigEntry( entry = MockConfigEntry(
version=1, version=2,
domain=zha_const.DOMAIN, domain=zha_const.DOMAIN,
data={ data={
zha_const.CONF_BAUDRATE: zha_const.DEFAULT_BAUDRATE, zigpy.config.CONF_DEVICE: {zigpy.config.CONF_DEVICE_PATH: "/dev/ttyUSB0"},
zha_const.CONF_RADIO_TYPE: "MockRadio", zha_const.CONF_RADIO_TYPE: "MockRadio",
zha_const.CONF_USB_PATH: "/dev/ttyUSB0",
}, },
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)
@ -65,10 +65,13 @@ async def config_entry_fixture(hass):
def setup_zha(hass, config_entry, zigpy_app_controller, zigpy_radio): def setup_zha(hass, config_entry, zigpy_app_controller, zigpy_radio):
"""Set up ZHA component.""" """Set up ZHA component."""
zha_config = {zha_const.CONF_ENABLE_QUIRKS: False} zha_config = {zha_const.CONF_ENABLE_QUIRKS: False}
app_ctrl = mock.MagicMock()
app_ctrl.new = tests.async_mock.AsyncMock(return_value=zigpy_app_controller)
app_ctrl.SCHEMA = zigpy.config.CONFIG_SCHEMA
app_ctrl.SCHEMA_DEVICE = zigpy.config.SCHEMA_DEVICE
radio_details = { radio_details = {
zha_const.ZHA_GW_RADIO: mock.MagicMock(return_value=zigpy_radio), zha_const.CONTROLLER: app_ctrl,
zha_const.CONTROLLER: mock.MagicMock(return_value=zigpy_app_controller),
zha_const.ZHA_GW_RADIO_DESCRIPTION: "mock radio", zha_const.ZHA_GW_RADIO_DESCRIPTION: "mock radio",
} }

View file

@ -1,125 +1,237 @@
"""Tests for ZHA config flow.""" """Tests for ZHA config flow."""
from unittest import mock
import os
import pytest
import serial.tools.list_ports
import zigpy.config
from homeassistant import setup
from homeassistant.components.zha import config_flow from homeassistant.components.zha import config_flow
from homeassistant.components.zha.core.const import CONTROLLER, DOMAIN, ZHA_GW_RADIO from homeassistant.components.zha.core.const import CONF_RADIO_TYPE, CONTROLLER, DOMAIN
import homeassistant.components.zha.core.registries from homeassistant.components.zha.core.registries import RADIO_TYPES
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_SOURCE
from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM
import tests.async_mock from tests.async_mock import AsyncMock, MagicMock, patch, sentinel
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
async def test_user_flow(hass): def com_port():
"""Test that config flow works.""" """Mock of a serial port."""
flow = config_flow.ZhaFlowHandler() port = serial.tools.list_ports_common.ListPortInfo()
flow.hass = hass port.serial_number = "1234"
port.manufacturer = "Virtual serial port"
port.device = "/dev/ttyUSB1234"
port.description = "Some serial port"
with tests.async_mock.patch( return port
"homeassistant.components.zha.config_flow.check_zigpy_connection",
return_value=False,
):
result = await flow.async_step_user(
user_input={"usb_path": "/dev/ttyUSB1", "radio_type": "ezsp"}
)
assert result["errors"] == {"base": "cannot_connect"}
with tests.async_mock.patch( @patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()]))
"homeassistant.components.zha.config_flow.check_zigpy_connection", @patch(
return_value=True, "homeassistant.components.zha.config_flow.detect_radios",
): return_value={CONF_RADIO_TYPE: "test_radio"},
result = await flow.async_step_user( )
user_input={"usb_path": "/dev/ttyUSB1", "radio_type": "ezsp"} async def test_user_flow(detect_mock, hass):
) """Test user flow -- radio detected."""
assert result["type"] == "create_entry" port = com_port()
assert result["title"] == "/dev/ttyUSB1" port_select = f"{port}, s/n: {port.serial_number} - {port.manufacturer}"
assert result["data"] == {"usb_path": "/dev/ttyUSB1", "radio_type": "ezsp"}
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_USER},
data={zigpy.config.CONF_DEVICE_PATH: port_select},
)
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"].startswith(port.description)
assert result["data"] == {CONF_RADIO_TYPE: "test_radio"}
assert detect_mock.await_count == 1
assert detect_mock.await_args[0][0] == port.device
@patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()]))
@patch(
"homeassistant.components.zha.config_flow.detect_radios", return_value=None,
)
async def test_user_flow_not_detected(detect_mock, hass):
"""Test user flow, radio not detected."""
port = com_port()
port_select = f"{port}, s/n: {port.serial_number} - {port.manufacturer}"
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_USER},
data={zigpy.config.CONF_DEVICE_PATH: port_select},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "pick_radio"
assert detect_mock.await_count == 1
assert detect_mock.await_args[0][0] == port.device
async def test_user_flow_show_form(hass):
"""Test user step form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "user"
async def test_user_flow_manual(hass):
"""Test user flow manual entry."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_USER},
data={zigpy.config.CONF_DEVICE_PATH: config_flow.CONF_MANUAL_PATH},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "pick_radio"
async def test_pick_radio_flow(hass):
"""Test radio picker."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: "pick_radio"}, data={CONF_RADIO_TYPE: "ezsp"}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "port_config"
async def test_user_flow_existing_config_entry(hass): async def test_user_flow_existing_config_entry(hass):
"""Test if config entry already exists.""" """Test if config entry already exists."""
MockConfigEntry(domain=DOMAIN, data={"usb_path": "/dev/ttyUSB1"}).add_to_hass(hass) MockConfigEntry(domain=DOMAIN, data={"usb_path": "/dev/ttyUSB1"}).add_to_hass(hass)
flow = config_flow.ZhaFlowHandler() await setup.async_setup_component(hass, "persistent_notification", {})
flow.hass = hass result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}
result = await flow.async_step_user()
assert result["type"] == "abort"
async def test_import_flow(hass):
"""Test import from configuration.yaml ."""
flow = config_flow.ZhaFlowHandler()
flow.hass = hass
result = await flow.async_step_import(
{"usb_path": "/dev/ttyUSB1", "radio_type": "xbee"}
)
assert result["type"] == "create_entry"
assert result["title"] == "/dev/ttyUSB1"
assert result["data"] == {"usb_path": "/dev/ttyUSB1", "radio_type": "xbee"}
async def test_import_flow_existing_config_entry(hass):
"""Test import from configuration.yaml ."""
MockConfigEntry(domain=DOMAIN, data={"usb_path": "/dev/ttyUSB1"}).add_to_hass(hass)
flow = config_flow.ZhaFlowHandler()
flow.hass = hass
result = await flow.async_step_import(
{"usb_path": "/dev/ttyUSB1", "radio_type": "xbee"}
) )
assert result["type"] == "abort" assert result["type"] == "abort"
async def test_check_zigpy_connection(): async def test_probe_radios(hass):
"""Test config flow validator.""" """Test detect radios."""
app_ctrl_cls = MagicMock()
app_ctrl_cls.SCHEMA_DEVICE = zigpy.config.SCHEMA_DEVICE
app_ctrl_cls.probe = AsyncMock(side_effect=(True, False))
mock_radio = tests.async_mock.MagicMock() with patch.dict(config_flow.RADIO_TYPES, {"ezsp": {CONTROLLER: app_ctrl_cls}}):
mock_radio.connect = tests.async_mock.AsyncMock() res = await config_flow.detect_radios("/dev/null")
radio_cls = tests.async_mock.MagicMock(return_value=mock_radio) assert app_ctrl_cls.probe.await_count == 1
assert res[CONF_RADIO_TYPE] == "ezsp"
assert zigpy.config.CONF_DEVICE in res
assert (
res[zigpy.config.CONF_DEVICE][zigpy.config.CONF_DEVICE_PATH] == "/dev/null"
)
bad_radio = tests.async_mock.MagicMock() res = await config_flow.detect_radios("/dev/null")
bad_radio.connect = tests.async_mock.AsyncMock(side_effect=Exception) assert res is None
bad_radio_cls = tests.async_mock.MagicMock(return_value=bad_radio)
mock_ctrl = tests.async_mock.MagicMock()
mock_ctrl.startup = tests.async_mock.AsyncMock()
mock_ctrl.shutdown = tests.async_mock.AsyncMock()
ctrl_cls = tests.async_mock.MagicMock(return_value=mock_ctrl)
new_radios = {
mock.sentinel.radio: {ZHA_GW_RADIO: radio_cls, CONTROLLER: ctrl_cls},
mock.sentinel.bad_radio: {ZHA_GW_RADIO: bad_radio_cls, CONTROLLER: ctrl_cls},
}
with mock.patch.dict( async def test_user_port_config_fail(hass):
homeassistant.components.zha.core.registries.RADIO_TYPES, new_radios, clear=True """Test port config flow."""
app_ctrl_cls = MagicMock()
app_ctrl_cls.SCHEMA_DEVICE = zigpy.config.SCHEMA_DEVICE
app_ctrl_cls.probe = AsyncMock(return_value=False)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: "pick_radio"}, data={CONF_RADIO_TYPE: "ezsp"}
)
with patch.dict(config_flow.RADIO_TYPES, {"ezsp": {CONTROLLER: app_ctrl_cls}}):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={zigpy.config.CONF_DEVICE_PATH: "/dev/ttyUSB33"},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "port_config"
assert result["errors"]["base"] == "cannot_connect"
@pytest.mark.parametrize(
"radio_type, orig_ctrl_cls",
((name, r[CONTROLLER]) for name, r in RADIO_TYPES.items()),
)
@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True))
async def test_user_port_config(hass, radio_type, orig_ctrl_cls):
"""Test port config."""
app_ctrl_cls = MagicMock()
app_ctrl_cls.SCHEMA_DEVICE = orig_ctrl_cls.SCHEMA_DEVICE
app_ctrl_cls.probe = AsyncMock(return_value=True)
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: "pick_radio"}, data={CONF_RADIO_TYPE: radio_type}
)
with patch.dict(
config_flow.RADIO_TYPES,
{radio_type: {CONTROLLER: app_ctrl_cls, "radio_description": "radio"}},
): ):
assert not await config_flow.check_zigpy_connection( result = await hass.config_entries.flow.async_configure(
mock.sentinel.usb_path, mock.sentinel.unk_radio, mock.sentinel.zigbee_db result["flow_id"],
user_input={zigpy.config.CONF_DEVICE_PATH: "/dev/ttyUSB33"},
) )
assert mock_radio.connect.call_count == 0
assert bad_radio.connect.call_count == 0
assert mock_ctrl.startup.call_count == 0
assert mock_ctrl.shutdown.call_count == 0
# unsuccessful radio connect assert result["type"] == "create_entry"
assert not await config_flow.check_zigpy_connection( assert result["title"].startswith("/dev/ttyUSB33")
mock.sentinel.usb_path, mock.sentinel.bad_radio, mock.sentinel.zigbee_db assert (
result["data"][zigpy.config.CONF_DEVICE][zigpy.config.CONF_DEVICE_PATH]
== "/dev/ttyUSB33"
) )
assert mock_radio.connect.call_count == 0 assert result["data"][CONF_RADIO_TYPE] == radio_type
assert bad_radio.connect.call_count == 1
assert mock_ctrl.startup.call_count == 0
assert mock_ctrl.shutdown.call_count == 0
# successful radio connect
assert await config_flow.check_zigpy_connection( def test_get_serial_by_id_no_dir():
mock.sentinel.usb_path, mock.sentinel.radio, mock.sentinel.zigbee_db """Test serial by id conversion if there's no /dev/serial/by-id."""
) p1 = patch("os.path.isdir", MagicMock(return_value=False))
assert mock_radio.connect.call_count == 1 p2 = patch("os.scandir")
assert bad_radio.connect.call_count == 1 with p1 as is_dir_mock, p2 as scan_mock:
assert mock_ctrl.startup.call_count == 1 res = config_flow.get_serial_by_id(sentinel.path)
assert mock_ctrl.shutdown.call_count == 1 assert res is sentinel.path
assert is_dir_mock.call_count == 1
assert scan_mock.call_count == 0
def test_get_serial_by_id():
"""Test serial by id conversion."""
p1 = patch("os.path.isdir", MagicMock(return_value=True))
p2 = patch("os.scandir")
def _realpath(path):
if path is sentinel.matched_link:
return sentinel.path
return sentinel.serial_link_path
p3 = patch("os.path.realpath", side_effect=_realpath)
with p1 as is_dir_mock, p2 as scan_mock, p3:
res = config_flow.get_serial_by_id(sentinel.path)
assert res is sentinel.path
assert is_dir_mock.call_count == 1
assert scan_mock.call_count == 1
entry1 = MagicMock(spec_set=os.DirEntry)
entry1.is_symlink.return_value = True
entry1.path = sentinel.some_path
entry2 = MagicMock(spec_set=os.DirEntry)
entry2.is_symlink.return_value = False
entry2.path = sentinel.other_path
entry3 = MagicMock(spec_set=os.DirEntry)
entry3.is_symlink.return_value = True
entry3.path = sentinel.matched_link
scan_mock.return_value = [entry1, entry2, entry3]
res = config_flow.get_serial_by_id(sentinel.path)
assert res is sentinel.matched_link
assert is_dir_mock.call_count == 2
assert scan_mock.call_count == 2

View file

@ -0,0 +1,72 @@
"""Tests for ZHA integration init."""
import pytest
from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH
from homeassistant.components.zha.core.const import (
CONF_BAUDRATE,
CONF_RADIO_TYPE,
CONF_USB_PATH,
DOMAIN,
)
from homeassistant.setup import async_setup_component
from tests.async_mock import AsyncMock, patch
from tests.common import MockConfigEntry
DATA_RADIO_TYPE = "deconz"
DATA_PORT_PATH = "/dev/serial/by-id/FTDI_USB__-__Serial_Cable_12345678-if00-port0"
@pytest.fixture
def config_entry_v1(hass):
"""Config entry version 1 fixture."""
return MockConfigEntry(
domain=DOMAIN,
data={CONF_RADIO_TYPE: DATA_RADIO_TYPE, CONF_USB_PATH: DATA_PORT_PATH},
version=1,
)
@pytest.mark.parametrize("config", ({}, {DOMAIN: {}}))
@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True))
async def test_migration_from_v1_no_baudrate(hass, config_entry_v1, config):
"""Test migration of config entry from v1."""
config_entry_v1.add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, config)
assert config_entry_v1.data[CONF_RADIO_TYPE] == DATA_RADIO_TYPE
assert CONF_DEVICE in config_entry_v1.data
assert config_entry_v1.data[CONF_DEVICE][CONF_DEVICE_PATH] == DATA_PORT_PATH
assert CONF_BAUDRATE not in config_entry_v1.data[CONF_DEVICE]
assert CONF_USB_PATH not in config_entry_v1.data
assert config_entry_v1.version == 2
@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True))
async def test_migration_from_v1_with_baudrate(hass, config_entry_v1):
"""Test migration of config entry from v1 with baudrate in config."""
config_entry_v1.add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_BAUDRATE: 115200}})
assert config_entry_v1.data[CONF_RADIO_TYPE] == DATA_RADIO_TYPE
assert CONF_DEVICE in config_entry_v1.data
assert config_entry_v1.data[CONF_DEVICE][CONF_DEVICE_PATH] == DATA_PORT_PATH
assert CONF_USB_PATH not in config_entry_v1.data
assert CONF_BAUDRATE in config_entry_v1.data[CONF_DEVICE]
assert config_entry_v1.data[CONF_DEVICE][CONF_BAUDRATE] == 115200
assert config_entry_v1.version == 2
@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True))
async def test_migration_from_v1_wrong_baudrate(hass, config_entry_v1):
"""Test migration of config entry from v1 with wrong baudrate."""
config_entry_v1.add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_BAUDRATE: 115222}})
assert config_entry_v1.data[CONF_RADIO_TYPE] == DATA_RADIO_TYPE
assert CONF_DEVICE in config_entry_v1.data
assert config_entry_v1.data[CONF_DEVICE][CONF_DEVICE_PATH] == DATA_PORT_PATH
assert CONF_USB_PATH not in config_entry_v1.data
assert CONF_BAUDRATE not in config_entry_v1.data[CONF_DEVICE]
assert config_entry_v1.version == 2