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 entrypull/18754/head
parent
43676fcaf4
commit
052d305243
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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 = {}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -158,6 +158,7 @@ FLOWS = [
|
|||
'twilio',
|
||||
'unifi',
|
||||
'upnp',
|
||||
'zha',
|
||||
'zone',
|
||||
'zwave'
|
||||
]
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
"""Tests for the ZHA component."""
|
|
@ -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'
|
Loading…
Reference in New Issue