Add config entry for ZHA (#18352)

* Add support for zha config entries

* Add support for zha config entries

* Fix node_config retrieval

* Dynamically load discovered entities

* Restore device config support

* Refactor loading of entities

* Remove device registry support

* Send discovery_info directly

* Clean up discovery_info in hass.data

* Update tests

* Clean up rebase

* Simplify config flow

* Address comments

* Fix config path and zigpy check timeout

* Remove device entities when unloading config entry
pull/18754/head
damarco 2018-11-27 21:21:25 +01:00 committed by Paulus Schoutsen
parent 43676fcaf4
commit 052d305243
14 changed files with 567 additions and 135 deletions

View File

@ -9,6 +9,10 @@ import logging
from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice
from homeassistant.components.zha.entities import ZhaEntity
from homeassistant.components.zha import helpers
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.zha.const import (
ZHA_DISCOVERY_NEW, DATA_ZHA, DATA_ZHA_DISPATCHERS
)
_LOGGER = logging.getLogger(__name__)
@ -27,23 +31,43 @@ CLASS_MAPPING = {
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the Zigbee Home Automation binary sensors."""
discovery_info = helpers.get_discovery_info(hass, discovery_info)
if discovery_info is None:
return
from zigpy.zcl.clusters.general import OnOff
from zigpy.zcl.clusters.security import IasZone
if IasZone.cluster_id in discovery_info['in_clusters']:
await _async_setup_iaszone(hass, config, async_add_entities,
discovery_info)
elif OnOff.cluster_id in discovery_info['out_clusters']:
await _async_setup_remote(hass, config, async_add_entities,
discovery_info)
"""Old way of setting up Zigbee Home Automation binary sensors."""
pass
async def _async_setup_iaszone(hass, config, async_add_entities,
discovery_info):
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Zigbee Home Automation binary sensor from config entry."""
async def async_discover(discovery_info):
await _async_setup_entities(hass, config_entry, async_add_entities,
[discovery_info])
unsub = async_dispatcher_connect(
hass, ZHA_DISCOVERY_NEW.format(DOMAIN), async_discover)
hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub)
binary_sensors = hass.data.get(DATA_ZHA, {}).get(DOMAIN)
if binary_sensors is not None:
await _async_setup_entities(hass, config_entry, async_add_entities,
binary_sensors.values())
del hass.data[DATA_ZHA][DOMAIN]
async def _async_setup_entities(hass, config_entry, async_add_entities,
discovery_infos):
"""Set up the ZHA binary sensors."""
entities = []
for discovery_info in discovery_infos:
from zigpy.zcl.clusters.general import OnOff
from zigpy.zcl.clusters.security import IasZone
if IasZone.cluster_id in discovery_info['in_clusters']:
entities.append(await _async_setup_iaszone(discovery_info))
elif OnOff.cluster_id in discovery_info['out_clusters']:
entities.append(await _async_setup_remote(discovery_info))
async_add_entities(entities, update_before_add=True)
async def _async_setup_iaszone(discovery_info):
device_class = None
from zigpy.zcl.clusters.security import IasZone
cluster = discovery_info['in_clusters'][IasZone.cluster_id]
@ -59,13 +83,10 @@ async def _async_setup_iaszone(hass, config, async_add_entities,
# If we fail to read from the device, use a non-specific class
pass
sensor = BinarySensor(device_class, **discovery_info)
async_add_entities([sensor], update_before_add=True)
return BinarySensor(device_class, **discovery_info)
async def _async_setup_remote(hass, config, async_add_entities,
discovery_info):
async def _async_setup_remote(discovery_info):
remote = Remote(**discovery_info)
if discovery_info['new_join']:
@ -84,7 +105,7 @@ async def _async_setup_remote(hass, config, async_add_entities,
reportable_change=1
)
async_add_entities([remote], update_before_add=True)
return remote
class BinarySensor(ZhaEntity, BinarySensorDevice):

View File

@ -7,6 +7,10 @@ at https://home-assistant.io/components/fan.zha/
import logging
from homeassistant.components.zha.entities import ZhaEntity
from homeassistant.components.zha import helpers
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.zha.const import (
ZHA_DISCOVERY_NEW, DATA_ZHA, DATA_ZHA_DISPATCHERS
)
from homeassistant.components.fan import (
DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
SUPPORT_SET_SPEED)
@ -40,12 +44,35 @@ SPEED_TO_VALUE = {speed: i for i, speed in enumerate(SPEED_LIST)}
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the Zigbee Home Automation fans."""
discovery_info = helpers.get_discovery_info(hass, discovery_info)
if discovery_info is None:
return
"""Old way of setting up Zigbee Home Automation fans."""
pass
async_add_entities([ZhaFan(**discovery_info)], update_before_add=True)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Zigbee Home Automation fan from config entry."""
async def async_discover(discovery_info):
await _async_setup_entities(hass, config_entry, async_add_entities,
[discovery_info])
unsub = async_dispatcher_connect(
hass, ZHA_DISCOVERY_NEW.format(DOMAIN), async_discover)
hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub)
fans = hass.data.get(DATA_ZHA, {}).get(DOMAIN)
if fans is not None:
await _async_setup_entities(hass, config_entry, async_add_entities,
fans.values())
del hass.data[DATA_ZHA][DOMAIN]
async def _async_setup_entities(hass, config_entry, async_add_entities,
discovery_infos):
"""Set up the ZHA fans."""
entities = []
for discovery_info in discovery_infos:
entities.append(ZhaFan(**discovery_info))
async_add_entities(entities, update_before_add=True)
class ZhaFan(ZhaEntity, FanEntity):

View File

@ -8,6 +8,10 @@ import logging
from homeassistant.components import light
from homeassistant.components.zha.entities import ZhaEntity
from homeassistant.components.zha import helpers
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.zha.const import (
ZHA_DISCOVERY_NEW, DATA_ZHA, DATA_ZHA_DISPATCHERS
)
import homeassistant.util.color as color_util
_LOGGER = logging.getLogger(__name__)
@ -24,27 +28,54 @@ UNSUPPORTED_ATTRIBUTE = 0x86
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the Zigbee Home Automation lights."""
discovery_info = helpers.get_discovery_info(hass, discovery_info)
if discovery_info is None:
return
"""Old way of setting up Zigbee Home Automation lights."""
pass
endpoint = discovery_info['endpoint']
if hasattr(endpoint, 'light_color'):
caps = await helpers.safe_read(
endpoint.light_color, ['color_capabilities'])
discovery_info['color_capabilities'] = caps.get('color_capabilities')
if discovery_info['color_capabilities'] is None:
# ZCL Version 4 devices don't support the color_capabilities
# attribute. In this version XY support is mandatory, but we need
# to probe to determine if the device supports color temperature.
discovery_info['color_capabilities'] = CAPABILITIES_COLOR_XY
result = await helpers.safe_read(
endpoint.light_color, ['color_temperature'])
if result.get('color_temperature') is not UNSUPPORTED_ATTRIBUTE:
discovery_info['color_capabilities'] |= CAPABILITIES_COLOR_TEMP
async_add_entities([Light(**discovery_info)], update_before_add=True)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Zigbee Home Automation light from config entry."""
async def async_discover(discovery_info):
await _async_setup_entities(hass, config_entry, async_add_entities,
[discovery_info])
unsub = async_dispatcher_connect(
hass, ZHA_DISCOVERY_NEW.format(light.DOMAIN), async_discover)
hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub)
lights = hass.data.get(DATA_ZHA, {}).get(light.DOMAIN)
if lights is not None:
await _async_setup_entities(hass, config_entry, async_add_entities,
lights.values())
del hass.data[DATA_ZHA][light.DOMAIN]
async def _async_setup_entities(hass, config_entry, async_add_entities,
discovery_infos):
"""Set up the ZHA lights."""
entities = []
for discovery_info in discovery_infos:
endpoint = discovery_info['endpoint']
if hasattr(endpoint, 'light_color'):
caps = await helpers.safe_read(
endpoint.light_color, ['color_capabilities'])
discovery_info['color_capabilities'] = caps.get(
'color_capabilities')
if discovery_info['color_capabilities'] is None:
# ZCL Version 4 devices don't support the color_capabilities
# attribute. In this version XY support is mandatory, but we
# need to probe to determine if the device supports color
# temperature.
discovery_info['color_capabilities'] = \
CAPABILITIES_COLOR_XY
result = await helpers.safe_read(
endpoint.light_color, ['color_temperature'])
if (result.get('color_temperature') is not
UNSUPPORTED_ATTRIBUTE):
discovery_info['color_capabilities'] |= \
CAPABILITIES_COLOR_TEMP
entities.append(Light(**discovery_info))
async_add_entities(entities, update_before_add=True)
class Light(ZhaEntity, light.Light):

View File

@ -9,6 +9,10 @@ import logging
from homeassistant.components.sensor import DOMAIN
from homeassistant.components.zha.entities import ZhaEntity
from homeassistant.components.zha import helpers
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.zha.const import (
ZHA_DISCOVERY_NEW, DATA_ZHA, DATA_ZHA_DISPATCHERS
)
from homeassistant.const import TEMP_CELSIUS
from homeassistant.util.temperature import convert as convert_temperature
@ -19,13 +23,35 @@ DEPENDENCIES = ['zha']
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up Zigbee Home Automation sensors."""
discovery_info = helpers.get_discovery_info(hass, discovery_info)
if discovery_info is None:
return
"""Old way of setting up Zigbee Home Automation sensors."""
pass
sensor = await make_sensor(discovery_info)
async_add_entities([sensor], update_before_add=True)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Zigbee Home Automation sensor from config entry."""
async def async_discover(discovery_info):
await _async_setup_entities(hass, config_entry, async_add_entities,
[discovery_info])
unsub = async_dispatcher_connect(
hass, ZHA_DISCOVERY_NEW.format(DOMAIN), async_discover)
hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub)
sensors = hass.data.get(DATA_ZHA, {}).get(DOMAIN)
if sensors is not None:
await _async_setup_entities(hass, config_entry, async_add_entities,
sensors.values())
del hass.data[DATA_ZHA][DOMAIN]
async def _async_setup_entities(hass, config_entry, async_add_entities,
discovery_infos):
"""Set up the ZHA sensors."""
entities = []
for discovery_info in discovery_infos:
entities.append(await make_sensor(discovery_info))
async_add_entities(entities, update_before_add=True)
async def make_sensor(discovery_info):

View File

@ -6,9 +6,13 @@ at https://home-assistant.io/components/switch.zha/
"""
import logging
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.switch import DOMAIN, SwitchDevice
from homeassistant.components.zha.entities import ZhaEntity
from homeassistant.components.zha import helpers
from homeassistant.components.zha.const import (
ZHA_DISCOVERY_NEW, DATA_ZHA, DATA_ZHA_DISPATCHERS
)
_LOGGER = logging.getLogger(__name__)
@ -17,24 +21,44 @@ DEPENDENCIES = ['zha']
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the Zigbee Home Automation switches."""
"""Old way of setting up Zigbee Home Automation switches."""
pass
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Zigbee Home Automation switch from config entry."""
async def async_discover(discovery_info):
await _async_setup_entities(hass, config_entry, async_add_entities,
[discovery_info])
unsub = async_dispatcher_connect(
hass, ZHA_DISCOVERY_NEW.format(DOMAIN), async_discover)
hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub)
switches = hass.data.get(DATA_ZHA, {}).get(DOMAIN)
if switches is not None:
await _async_setup_entities(hass, config_entry, async_add_entities,
switches.values())
del hass.data[DATA_ZHA][DOMAIN]
async def _async_setup_entities(hass, config_entry, async_add_entities,
discovery_infos):
"""Set up the ZHA switches."""
from zigpy.zcl.clusters.general import OnOff
entities = []
for discovery_info in discovery_infos:
switch = Switch(**discovery_info)
if discovery_info['new_join']:
in_clusters = discovery_info['in_clusters']
cluster = in_clusters[OnOff.cluster_id]
await helpers.configure_reporting(
switch.entity_id, cluster, switch.value_attribute,
min_report=0, max_report=600, reportable_change=1
)
entities.append(switch)
discovery_info = helpers.get_discovery_info(hass, discovery_info)
if discovery_info is None:
return
switch = Switch(**discovery_info)
if discovery_info['new_join']:
in_clusters = discovery_info['in_clusters']
cluster = in_clusters[OnOff.cluster_id]
await helpers.configure_reporting(
switch.entity_id, cluster, switch.value_attribute,
min_report=0, max_report=600, reportable_change=1
)
async_add_entities([switch], update_before_add=True)
async_add_entities(entities, update_before_add=True)
class Switch(ZhaEntity, SwitchDevice):

View File

@ -0,0 +1,21 @@
{
"config": {
"title": "ZHA",
"step": {
"user": {
"title": "ZHA",
"description": "",
"data": {
"usb_path": "USB Device Path",
"radio_type": "Radio Type"
}
}
},
"abort": {
"single_instance_allowed": "Only a single configuration of ZHA is allowed."
},
"error": {
"cannot_connect": "Unable to connect to ZHA device."
}
}
}

View File

@ -5,51 +5,47 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/
"""
import collections
import enum
import logging
import os
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant import const as ha_const
from homeassistant.helpers import discovery
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components.zha.entities import ZhaDeviceEntity
from homeassistant import config_entries, const as ha_const
from homeassistant.helpers.dispatcher import async_dispatcher_send
from . import const as zha_const
# Loading the config flow file will register the flow
from . import config_flow # noqa # pylint: disable=unused-import
from .const import (
DOMAIN, COMPONENTS, CONF_BAUDRATE, CONF_DATABASE, CONF_RADIO_TYPE,
CONF_USB_PATH, CONF_DEVICE_CONFIG, ZHA_DISCOVERY_NEW, DATA_ZHA,
DATA_ZHA_CONFIG, DATA_ZHA_BRIDGE_ID, DATA_ZHA_RADIO, DATA_ZHA_DISPATCHERS,
DATA_ZHA_CORE_COMPONENT, DEFAULT_RADIO_TYPE, DEFAULT_DATABASE_NAME,
DEFAULT_BAUDRATE, RadioType
)
REQUIREMENTS = [
'bellows==0.7.0',
'zigpy==0.2.0',
'zigpy-xbee==0.1.1',
]
DOMAIN = 'zha'
class RadioType(enum.Enum):
"""Possible options for radio type in config."""
ezsp = 'ezsp'
xbee = 'xbee'
CONF_BAUDRATE = 'baudrate'
CONF_DATABASE = 'database_path'
CONF_DEVICE_CONFIG = 'device_config'
CONF_RADIO_TYPE = 'radio_type'
CONF_USB_PATH = 'usb_path'
DATA_DEVICE_CONFIG = 'zha_device_config'
DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({
vol.Optional(ha_const.CONF_TYPE): cv.string,
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_RADIO_TYPE, default='ezsp'): cv.enum(RadioType),
vol.Optional(
CONF_RADIO_TYPE,
default=DEFAULT_RADIO_TYPE
): cv.enum(RadioType),
CONF_USB_PATH: cv.string,
vol.Optional(CONF_BAUDRATE, default=57600): cv.positive_int,
CONF_DATABASE: cv.string,
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}),
})
@ -73,8 +69,6 @@ SERVICE_SCHEMAS = {
# Zigbee definitions
CENTICELSIUS = 'C-100'
# Key in hass.data dict containing discovery info
DISCOVERY_KEY = 'zha_discovery_info'
# Internal definitions
APPLICATION_CONTROLLER = None
@ -82,27 +76,58 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup(hass, config):
"""Set up ZHA from config."""
hass.data[DATA_ZHA] = {}
if DOMAIN not in config:
return True
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
async def async_setup_entry(hass, config_entry):
"""Set up ZHA.
Will automatically load components to support devices found on the network.
"""
global APPLICATION_CONTROLLER
usb_path = config[DOMAIN].get(CONF_USB_PATH)
baudrate = config[DOMAIN].get(CONF_BAUDRATE)
radio_type = config[DOMAIN].get(CONF_RADIO_TYPE)
if radio_type == RadioType.ezsp:
hass.data[DATA_ZHA] = hass.data.get(DATA_ZHA, {})
hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS] = []
config = hass.data[DATA_ZHA].get(DATA_ZHA_CONFIG, {})
usb_path = config_entry.data.get(CONF_USB_PATH)
baudrate = config.get(CONF_BAUDRATE, DEFAULT_BAUDRATE)
radio_type = config_entry.data.get(CONF_RADIO_TYPE)
if radio_type == RadioType.ezsp.name:
import bellows.ezsp
from bellows.zigbee.application import ControllerApplication
radio = bellows.ezsp.EZSP()
elif radio_type == RadioType.xbee:
elif radio_type == RadioType.xbee.name:
import zigpy_xbee.api
from zigpy_xbee.zigbee.application import ControllerApplication
radio = zigpy_xbee.api.XBee()
await radio.connect(usb_path, baudrate)
hass.data[DATA_ZHA][DATA_ZHA_RADIO] = radio
database = config[DOMAIN].get(CONF_DATABASE)
if CONF_DATABASE in config:
database = config[CONF_DATABASE]
else:
database = os.path.join(hass.config.config_dir, DEFAULT_DATABASE_NAME)
APPLICATION_CONTROLLER = ControllerApplication(radio, database)
listener = ApplicationListener(hass, config)
APPLICATION_CONTROLLER.add_listener(listener)
@ -112,6 +137,14 @@ async def async_setup(hass, config):
hass.async_create_task(
listener.async_device_initialized(device, False))
hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(APPLICATION_CONTROLLER.ieee)
for component in COMPONENTS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(
config_entry, component)
)
async def permit(service):
"""Allow devices to join this network."""
duration = service.data.get(ATTR_DURATION)
@ -132,6 +165,37 @@ async def async_setup(hass, config):
hass.services.async_register(DOMAIN, SERVICE_REMOVE, remove,
schema=SERVICE_SCHEMAS[SERVICE_REMOVE])
def zha_shutdown(event):
"""Close radio."""
hass.data[DATA_ZHA][DATA_ZHA_RADIO].close()
hass.bus.async_listen_once(ha_const.EVENT_HOMEASSISTANT_STOP, zha_shutdown)
return True
async def async_unload_entry(hass, config_entry):
"""Unload ZHA config entry."""
hass.services.async_remove(DOMAIN, SERVICE_PERMIT)
hass.services.async_remove(DOMAIN, SERVICE_REMOVE)
dispatchers = hass.data[DATA_ZHA].get(DATA_ZHA_DISPATCHERS, [])
for unsub_dispatcher in dispatchers:
unsub_dispatcher()
for component in COMPONENTS:
await hass.config_entries.async_forward_entry_unload(
config_entry, component)
# clean up device entities
component = hass.data[DATA_ZHA][DATA_ZHA_CORE_COMPONENT]
entity_ids = [entity.entity_id for entity in component.entities]
for entity_id in entity_ids:
await component.async_remove_entity(entity_id)
_LOGGER.debug("Closing zha radio")
hass.data[DATA_ZHA][DATA_ZHA_RADIO].close()
del hass.data[DATA_ZHA]
return True
@ -144,9 +208,14 @@ class ApplicationListener:
self._config = config
self._component = EntityComponent(_LOGGER, DOMAIN, hass)
self._device_registry = collections.defaultdict(list)
hass.data[DISCOVERY_KEY] = hass.data.get(DISCOVERY_KEY, {})
zha_const.populate_data()
for component in COMPONENTS:
hass.data[DATA_ZHA][component] = (
hass.data[DATA_ZHA].get(component, {})
)
hass.data[DATA_ZHA][DATA_ZHA_CORE_COMPONENT] = self._component
def device_joined(self, device):
"""Handle device joined.
@ -193,8 +262,11 @@ class ApplicationListener:
component = None
profile_clusters = ([], [])
device_key = "{}-{}".format(device.ieee, endpoint_id)
node_config = self._config[DOMAIN][CONF_DEVICE_CONFIG].get(
device_key, {})
node_config = {}
if CONF_DEVICE_CONFIG in self._config:
node_config = self._config[CONF_DEVICE_CONFIG].get(
device_key, {}
)
if endpoint.profile_id in zigpy.profiles.PROFILES:
profile = zigpy.profiles.PROFILES[endpoint.profile_id]
@ -226,15 +298,17 @@ class ApplicationListener:
'new_join': join,
'unique_id': device_key,
}
self._hass.data[DISCOVERY_KEY][device_key] = discovery_info
await discovery.async_load_platform(
self._hass,
component,
DOMAIN,
{'discovery_key': device_key},
self._config,
)
if join:
async_dispatcher_send(
self._hass,
ZHA_DISCOVERY_NEW.format(component),
discovery_info
)
else:
self._hass.data[DATA_ZHA][component][device_key] = (
discovery_info
)
for cluster in endpoint.in_clusters.values():
await self._attempt_single_cluster_device(
@ -309,12 +383,12 @@ class ApplicationListener:
discovery_info[discovery_attr] = {cluster.cluster_id: cluster}
if sub_component:
discovery_info.update({'sub_component': sub_component})
self._hass.data[DISCOVERY_KEY][cluster_key] = discovery_info
await discovery.async_load_platform(
self._hass,
component,
DOMAIN,
{'discovery_key': cluster_key},
self._config,
)
if is_new_join:
async_dispatcher_send(
self._hass,
ZHA_DISCOVERY_NEW.format(component),
discovery_info
)
else:
self._hass.data[DATA_ZHA][component][cluster_key] = discovery_info

View File

@ -0,0 +1,57 @@
"""Config flow for ZHA."""
import os
from collections import OrderedDict
import voluptuous as vol
from homeassistant import config_entries
from .helpers import check_zigpy_connection
from .const import (
DOMAIN, CONF_RADIO_TYPE, CONF_USB_PATH, DEFAULT_DATABASE_NAME, RadioType
)
@config_entries.HANDLERS.register(DOMAIN)
class ZhaFlowHandler(config_entries.ConfigFlow):
"""Handle a config flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
async def async_step_user(self, user_input=None):
"""Handle a zha config flow start."""
if self._async_current_entries():
return self.async_abort(reason='single_instance_allowed')
errors = {}
fields = OrderedDict()
fields[vol.Required(CONF_USB_PATH)] = str
fields[vol.Optional(CONF_RADIO_TYPE, default='ezsp')] = vol.In(
RadioType.list()
)
if user_input is not None:
database = os.path.join(self.hass.config.config_dir,
DEFAULT_DATABASE_NAME)
test = await check_zigpy_connection(user_input[CONF_USB_PATH],
user_input[CONF_RADIO_TYPE],
database)
if test:
return self.async_create_entry(
title=user_input[CONF_USB_PATH], data=user_input)
errors['base'] = 'cannot_connect'
return self.async_show_form(
step_id='user', data_schema=vol.Schema(fields), 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
)

View File

@ -1,4 +1,51 @@
"""All constants related to the ZHA component."""
import enum
DOMAIN = 'zha'
BAUD_RATES = [
2400, 4800, 9600, 14400, 19200, 38400, 57600, 115200, 128000, 256000
]
DATA_ZHA = 'zha'
DATA_ZHA_CONFIG = 'config'
DATA_ZHA_BRIDGE_ID = 'zha_bridge_id'
DATA_ZHA_RADIO = 'zha_radio'
DATA_ZHA_DISPATCHERS = 'zha_dispatchers'
DATA_ZHA_CORE_COMPONENT = 'zha_core_component'
ZHA_DISCOVERY_NEW = 'zha_discovery_new_{}'
COMPONENTS = [
'binary_sensor',
'fan',
'light',
'sensor',
'switch',
]
CONF_BAUDRATE = 'baudrate'
CONF_DATABASE = 'database_path'
CONF_DEVICE_CONFIG = 'device_config'
CONF_RADIO_TYPE = 'radio_type'
CONF_USB_PATH = 'usb_path'
DATA_DEVICE_CONFIG = 'zha_device_config'
DEFAULT_RADIO_TYPE = 'ezsp'
DEFAULT_BAUDRATE = 57600
DEFAULT_DATABASE_NAME = 'zigbee.db'
class RadioType(enum.Enum):
"""Possible options for radio type."""
ezsp = 'ezsp'
xbee = 'xbee'
@classmethod
def list(cls):
"""Return list of enum's values."""
return [e.value for e in RadioType]
DISCOVERY_KEY = 'zha_discovery_info'
DEVICE_CLASS = {}

View File

@ -5,28 +5,12 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/
"""
import logging
import asyncio
from .const import RadioType, DEFAULT_BAUDRATE
_LOGGER = logging.getLogger(__name__)
def get_discovery_info(hass, discovery_info):
"""Get the full discovery info for a device.
Some of the info that needs to be passed to platforms is not JSON
serializable, so it cannot be put in the discovery_info dictionary. This
component places that info we need to pass to the platform in hass.data,
and this function is a helper for platforms to retrieve the complete
discovery info.
"""
if discovery_info is None:
return
import homeassistant.components.zha.const as zha_const
discovery_key = discovery_info.get('discovery_key', None)
all_discovery_info = hass.data.get(zha_const.DISCOVERY_KEY, {})
return all_discovery_info.get(discovery_key, None)
async def safe_read(cluster, attributes, allow_cache=True, only_cache=False):
"""Swallow all exceptions from network read.
@ -82,3 +66,23 @@ async def configure_reporting(entity_id, cluster, attr, skip_bind=False,
"%s: failed to set reporting for '%s' attr on '%s' cluster: %s",
entity_id, attr_name, cluster_name, str(ex)
)
async def check_zigpy_connection(usb_path, radio_type, database_path):
"""Test zigpy radio connection."""
if radio_type == RadioType.ezsp.name:
import bellows.ezsp
from bellows.zigbee.application import ControllerApplication
radio = bellows.ezsp.EZSP()
elif radio_type == RadioType.xbee.name:
import zigpy_xbee.api
from zigpy_xbee.zigbee.application import ControllerApplication
radio = zigpy_xbee.api.XBee()
try:
await radio.connect(usb_path, DEFAULT_BAUDRATE)
controller = ControllerApplication(radio, database_path)
await asyncio.wait_for(controller.startup(auto_form=True), timeout=30)
radio.close()
except Exception: # pylint: disable=broad-except
return False
return True

View File

@ -0,0 +1,21 @@
{
"config": {
"title": "ZHA",
"step": {
"user": {
"title": "ZHA",
"description": "",
"data": {
"usb_path": "USB Device Path",
"radio_type": "Radio Type"
}
}
},
"abort": {
"single_instance_allowed": "Only a single configuration of ZHA is allowed."
},
"error": {
"cannot_connect": "Unable to connect to ZHA device."
}
}
}

View File

@ -158,6 +158,7 @@ FLOWS = [
'twilio',
'unifi',
'upnp',
'zha',
'zone',
'zwave'
]

View File

@ -0,0 +1 @@
"""Tests for the ZHA component."""

View File

@ -0,0 +1,77 @@
"""Tests for ZHA config flow."""
from asynctest import patch
from homeassistant.components.zha import config_flow
from homeassistant.components.zha.const import DOMAIN
from tests.common import MockConfigEntry
async def test_user_flow(hass):
"""Test that config flow works."""
flow = config_flow.ZhaFlowHandler()
flow.hass = hass
with patch('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 patch('homeassistant.components.zha.config_flow'
'.check_zigpy_connection', return_value=True):
result = await flow.async_step_user(
user_input={'usb_path': '/dev/ttyUSB1', 'radio_type': 'ezsp'})
assert result['type'] == 'create_entry'
assert result['title'] == '/dev/ttyUSB1'
assert result['data'] == {
'usb_path': '/dev/ttyUSB1',
'radio_type': 'ezsp'
}
async def test_user_flow_existing_config_entry(hass):
"""Test if config entry already exists."""
MockConfigEntry(domain=DOMAIN, data={
'usb_path': '/dev/ttyUSB1'
}).add_to_hass(hass)
flow = config_flow.ZhaFlowHandler()
flow.hass = hass
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'