mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
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
This commit is contained in:
parent
43676fcaf4
commit
052d305243
@ -9,6 +9,10 @@ import logging
|
|||||||
from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice
|
from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice
|
||||||
from homeassistant.components.zha.entities import ZhaEntity
|
from homeassistant.components.zha.entities import ZhaEntity
|
||||||
from homeassistant.components.zha import helpers
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -27,23 +31,43 @@ CLASS_MAPPING = {
|
|||||||
|
|
||||||
async def async_setup_platform(hass, config, async_add_entities,
|
async def async_setup_platform(hass, config, async_add_entities,
|
||||||
discovery_info=None):
|
discovery_info=None):
|
||||||
"""Set up the Zigbee Home Automation binary sensors."""
|
"""Old way of setting up Zigbee Home Automation binary sensors."""
|
||||||
discovery_info = helpers.get_discovery_info(hass, discovery_info)
|
pass
|
||||||
if discovery_info is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
|
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.general import OnOff
|
||||||
from zigpy.zcl.clusters.security import IasZone
|
from zigpy.zcl.clusters.security import IasZone
|
||||||
if IasZone.cluster_id in discovery_info['in_clusters']:
|
if IasZone.cluster_id in discovery_info['in_clusters']:
|
||||||
await _async_setup_iaszone(hass, config, async_add_entities,
|
entities.append(await _async_setup_iaszone(discovery_info))
|
||||||
discovery_info)
|
|
||||||
elif OnOff.cluster_id in discovery_info['out_clusters']:
|
elif OnOff.cluster_id in discovery_info['out_clusters']:
|
||||||
await _async_setup_remote(hass, config, async_add_entities,
|
entities.append(await _async_setup_remote(discovery_info))
|
||||||
discovery_info)
|
|
||||||
|
async_add_entities(entities, update_before_add=True)
|
||||||
|
|
||||||
|
|
||||||
async def _async_setup_iaszone(hass, config, async_add_entities,
|
async def _async_setup_iaszone(discovery_info):
|
||||||
discovery_info):
|
|
||||||
device_class = None
|
device_class = None
|
||||||
from zigpy.zcl.clusters.security import IasZone
|
from zigpy.zcl.clusters.security import IasZone
|
||||||
cluster = discovery_info['in_clusters'][IasZone.cluster_id]
|
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
|
# If we fail to read from the device, use a non-specific class
|
||||||
pass
|
pass
|
||||||
|
|
||||||
sensor = BinarySensor(device_class, **discovery_info)
|
return BinarySensor(device_class, **discovery_info)
|
||||||
async_add_entities([sensor], update_before_add=True)
|
|
||||||
|
|
||||||
|
|
||||||
async def _async_setup_remote(hass, config, async_add_entities,
|
async def _async_setup_remote(discovery_info):
|
||||||
discovery_info):
|
|
||||||
|
|
||||||
remote = Remote(**discovery_info)
|
remote = Remote(**discovery_info)
|
||||||
|
|
||||||
if discovery_info['new_join']:
|
if discovery_info['new_join']:
|
||||||
@ -84,7 +105,7 @@ async def _async_setup_remote(hass, config, async_add_entities,
|
|||||||
reportable_change=1
|
reportable_change=1
|
||||||
)
|
)
|
||||||
|
|
||||||
async_add_entities([remote], update_before_add=True)
|
return remote
|
||||||
|
|
||||||
|
|
||||||
class BinarySensor(ZhaEntity, BinarySensorDevice):
|
class BinarySensor(ZhaEntity, BinarySensorDevice):
|
||||||
|
@ -7,6 +7,10 @@ at https://home-assistant.io/components/fan.zha/
|
|||||||
import logging
|
import logging
|
||||||
from homeassistant.components.zha.entities import ZhaEntity
|
from homeassistant.components.zha.entities import ZhaEntity
|
||||||
from homeassistant.components.zha import helpers
|
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 (
|
from homeassistant.components.fan import (
|
||||||
DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
|
DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
|
||||||
SUPPORT_SET_SPEED)
|
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,
|
async def async_setup_platform(hass, config, async_add_entities,
|
||||||
discovery_info=None):
|
discovery_info=None):
|
||||||
"""Set up the Zigbee Home Automation fans."""
|
"""Old way of setting up Zigbee Home Automation fans."""
|
||||||
discovery_info = helpers.get_discovery_info(hass, discovery_info)
|
pass
|
||||||
if discovery_info is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
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):
|
class ZhaFan(ZhaEntity, FanEntity):
|
||||||
|
@ -8,6 +8,10 @@ import logging
|
|||||||
from homeassistant.components import light
|
from homeassistant.components import light
|
||||||
from homeassistant.components.zha.entities import ZhaEntity
|
from homeassistant.components.zha.entities import ZhaEntity
|
||||||
from homeassistant.components.zha import helpers
|
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
|
import homeassistant.util.color as color_util
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -24,27 +28,54 @@ UNSUPPORTED_ATTRIBUTE = 0x86
|
|||||||
|
|
||||||
async def async_setup_platform(hass, config, async_add_entities,
|
async def async_setup_platform(hass, config, async_add_entities,
|
||||||
discovery_info=None):
|
discovery_info=None):
|
||||||
"""Set up the Zigbee Home Automation lights."""
|
"""Old way of setting up Zigbee Home Automation lights."""
|
||||||
discovery_info = helpers.get_discovery_info(hass, discovery_info)
|
pass
|
||||||
if discovery_info is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
|
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']
|
endpoint = discovery_info['endpoint']
|
||||||
if hasattr(endpoint, 'light_color'):
|
if hasattr(endpoint, 'light_color'):
|
||||||
caps = await helpers.safe_read(
|
caps = await helpers.safe_read(
|
||||||
endpoint.light_color, ['color_capabilities'])
|
endpoint.light_color, ['color_capabilities'])
|
||||||
discovery_info['color_capabilities'] = caps.get('color_capabilities')
|
discovery_info['color_capabilities'] = caps.get(
|
||||||
|
'color_capabilities')
|
||||||
if discovery_info['color_capabilities'] is None:
|
if discovery_info['color_capabilities'] is None:
|
||||||
# ZCL Version 4 devices don't support the color_capabilities
|
# ZCL Version 4 devices don't support the color_capabilities
|
||||||
# attribute. In this version XY support is mandatory, but we need
|
# attribute. In this version XY support is mandatory, but we
|
||||||
# to probe to determine if the device supports color temperature.
|
# need to probe to determine if the device supports color
|
||||||
discovery_info['color_capabilities'] = CAPABILITIES_COLOR_XY
|
# temperature.
|
||||||
|
discovery_info['color_capabilities'] = \
|
||||||
|
CAPABILITIES_COLOR_XY
|
||||||
result = await helpers.safe_read(
|
result = await helpers.safe_read(
|
||||||
endpoint.light_color, ['color_temperature'])
|
endpoint.light_color, ['color_temperature'])
|
||||||
if result.get('color_temperature') is not UNSUPPORTED_ATTRIBUTE:
|
if (result.get('color_temperature') is not
|
||||||
discovery_info['color_capabilities'] |= CAPABILITIES_COLOR_TEMP
|
UNSUPPORTED_ATTRIBUTE):
|
||||||
|
discovery_info['color_capabilities'] |= \
|
||||||
|
CAPABILITIES_COLOR_TEMP
|
||||||
|
entities.append(Light(**discovery_info))
|
||||||
|
|
||||||
async_add_entities([Light(**discovery_info)], update_before_add=True)
|
async_add_entities(entities, update_before_add=True)
|
||||||
|
|
||||||
|
|
||||||
class Light(ZhaEntity, light.Light):
|
class Light(ZhaEntity, light.Light):
|
||||||
|
@ -9,6 +9,10 @@ import logging
|
|||||||
from homeassistant.components.sensor import DOMAIN
|
from homeassistant.components.sensor import DOMAIN
|
||||||
from homeassistant.components.zha.entities import ZhaEntity
|
from homeassistant.components.zha.entities import ZhaEntity
|
||||||
from homeassistant.components.zha import helpers
|
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.const import TEMP_CELSIUS
|
||||||
from homeassistant.util.temperature import convert as convert_temperature
|
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,
|
async def async_setup_platform(hass, config, async_add_entities,
|
||||||
discovery_info=None):
|
discovery_info=None):
|
||||||
"""Set up Zigbee Home Automation sensors."""
|
"""Old way of setting up Zigbee Home Automation sensors."""
|
||||||
discovery_info = helpers.get_discovery_info(hass, discovery_info)
|
pass
|
||||||
if discovery_info is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
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):
|
async def make_sensor(discovery_info):
|
||||||
|
@ -6,9 +6,13 @@ at https://home-assistant.io/components/switch.zha/
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.components.switch import DOMAIN, SwitchDevice
|
from homeassistant.components.switch import DOMAIN, SwitchDevice
|
||||||
from homeassistant.components.zha.entities import ZhaEntity
|
from homeassistant.components.zha.entities import ZhaEntity
|
||||||
from homeassistant.components.zha import helpers
|
from homeassistant.components.zha import helpers
|
||||||
|
from homeassistant.components.zha.const import (
|
||||||
|
ZHA_DISCOVERY_NEW, DATA_ZHA, DATA_ZHA_DISPATCHERS
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -17,15 +21,34 @@ DEPENDENCIES = ['zha']
|
|||||||
|
|
||||||
async def async_setup_platform(hass, config, async_add_entities,
|
async def async_setup_platform(hass, config, async_add_entities,
|
||||||
discovery_info=None):
|
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
|
from zigpy.zcl.clusters.general import OnOff
|
||||||
|
entities = []
|
||||||
discovery_info = helpers.get_discovery_info(hass, discovery_info)
|
for discovery_info in discovery_infos:
|
||||||
if discovery_info is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
switch = Switch(**discovery_info)
|
switch = Switch(**discovery_info)
|
||||||
|
|
||||||
if discovery_info['new_join']:
|
if discovery_info['new_join']:
|
||||||
in_clusters = discovery_info['in_clusters']
|
in_clusters = discovery_info['in_clusters']
|
||||||
cluster = in_clusters[OnOff.cluster_id]
|
cluster = in_clusters[OnOff.cluster_id]
|
||||||
@ -33,8 +56,9 @@ async def async_setup_platform(hass, config, async_add_entities,
|
|||||||
switch.entity_id, cluster, switch.value_attribute,
|
switch.entity_id, cluster, switch.value_attribute,
|
||||||
min_report=0, max_report=600, reportable_change=1
|
min_report=0, max_report=600, reportable_change=1
|
||||||
)
|
)
|
||||||
|
entities.append(switch)
|
||||||
|
|
||||||
async_add_entities([switch], update_before_add=True)
|
async_add_entities(entities, update_before_add=True)
|
||||||
|
|
||||||
|
|
||||||
class Switch(ZhaEntity, SwitchDevice):
|
class Switch(ZhaEntity, SwitchDevice):
|
||||||
|
21
homeassistant/components/zha/.translations/en.json
Normal file
21
homeassistant/components/zha/.translations/en.json
Normal 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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,51 +5,47 @@ For more details about this component, please refer to the documentation at
|
|||||||
https://home-assistant.io/components/zha/
|
https://home-assistant.io/components/zha/
|
||||||
"""
|
"""
|
||||||
import collections
|
import collections
|
||||||
import enum
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
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.helpers.entity_component import EntityComponent
|
||||||
from homeassistant.components.zha.entities import ZhaDeviceEntity
|
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
|
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 = [
|
REQUIREMENTS = [
|
||||||
'bellows==0.7.0',
|
'bellows==0.7.0',
|
||||||
'zigpy==0.2.0',
|
'zigpy==0.2.0',
|
||||||
'zigpy-xbee==0.1.1',
|
'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({
|
DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({
|
||||||
vol.Optional(ha_const.CONF_TYPE): cv.string,
|
vol.Optional(ha_const.CONF_TYPE): cv.string,
|
||||||
})
|
})
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: 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,
|
CONF_USB_PATH: cv.string,
|
||||||
vol.Optional(CONF_BAUDRATE, default=57600): cv.positive_int,
|
vol.Optional(CONF_BAUDRATE, default=DEFAULT_BAUDRATE): cv.positive_int,
|
||||||
CONF_DATABASE: cv.string,
|
vol.Optional(CONF_DATABASE): cv.string,
|
||||||
vol.Optional(CONF_DEVICE_CONFIG, default={}):
|
vol.Optional(CONF_DEVICE_CONFIG, default={}):
|
||||||
vol.Schema({cv.string: DEVICE_CONFIG_SCHEMA_ENTRY}),
|
vol.Schema({cv.string: DEVICE_CONFIG_SCHEMA_ENTRY}),
|
||||||
})
|
})
|
||||||
@ -73,8 +69,6 @@ SERVICE_SCHEMAS = {
|
|||||||
|
|
||||||
# Zigbee definitions
|
# Zigbee definitions
|
||||||
CENTICELSIUS = 'C-100'
|
CENTICELSIUS = 'C-100'
|
||||||
# Key in hass.data dict containing discovery info
|
|
||||||
DISCOVERY_KEY = 'zha_discovery_info'
|
|
||||||
|
|
||||||
# Internal definitions
|
# Internal definitions
|
||||||
APPLICATION_CONTROLLER = None
|
APPLICATION_CONTROLLER = None
|
||||||
@ -82,27 +76,58 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
async def async_setup(hass, config):
|
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.
|
"""Set up ZHA.
|
||||||
|
|
||||||
Will automatically load components to support devices found on the network.
|
Will automatically load components to support devices found on the network.
|
||||||
"""
|
"""
|
||||||
global APPLICATION_CONTROLLER
|
global APPLICATION_CONTROLLER
|
||||||
|
|
||||||
usb_path = config[DOMAIN].get(CONF_USB_PATH)
|
hass.data[DATA_ZHA] = hass.data.get(DATA_ZHA, {})
|
||||||
baudrate = config[DOMAIN].get(CONF_BAUDRATE)
|
hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS] = []
|
||||||
radio_type = config[DOMAIN].get(CONF_RADIO_TYPE)
|
|
||||||
if radio_type == RadioType.ezsp:
|
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
|
import bellows.ezsp
|
||||||
from bellows.zigbee.application import ControllerApplication
|
from bellows.zigbee.application import ControllerApplication
|
||||||
radio = bellows.ezsp.EZSP()
|
radio = bellows.ezsp.EZSP()
|
||||||
elif radio_type == RadioType.xbee:
|
elif radio_type == RadioType.xbee.name:
|
||||||
import zigpy_xbee.api
|
import zigpy_xbee.api
|
||||||
from zigpy_xbee.zigbee.application import ControllerApplication
|
from zigpy_xbee.zigbee.application import ControllerApplication
|
||||||
radio = zigpy_xbee.api.XBee()
|
radio = zigpy_xbee.api.XBee()
|
||||||
|
|
||||||
await radio.connect(usb_path, baudrate)
|
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)
|
APPLICATION_CONTROLLER = ControllerApplication(radio, database)
|
||||||
listener = ApplicationListener(hass, config)
|
listener = ApplicationListener(hass, config)
|
||||||
APPLICATION_CONTROLLER.add_listener(listener)
|
APPLICATION_CONTROLLER.add_listener(listener)
|
||||||
@ -112,6 +137,14 @@ async def async_setup(hass, config):
|
|||||||
hass.async_create_task(
|
hass.async_create_task(
|
||||||
listener.async_device_initialized(device, False))
|
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):
|
async def permit(service):
|
||||||
"""Allow devices to join this network."""
|
"""Allow devices to join this network."""
|
||||||
duration = service.data.get(ATTR_DURATION)
|
duration = service.data.get(ATTR_DURATION)
|
||||||
@ -132,6 +165,37 @@ async def async_setup(hass, config):
|
|||||||
hass.services.async_register(DOMAIN, SERVICE_REMOVE, remove,
|
hass.services.async_register(DOMAIN, SERVICE_REMOVE, remove,
|
||||||
schema=SERVICE_SCHEMAS[SERVICE_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
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -144,9 +208,14 @@ class ApplicationListener:
|
|||||||
self._config = config
|
self._config = config
|
||||||
self._component = EntityComponent(_LOGGER, DOMAIN, hass)
|
self._component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||||
self._device_registry = collections.defaultdict(list)
|
self._device_registry = collections.defaultdict(list)
|
||||||
hass.data[DISCOVERY_KEY] = hass.data.get(DISCOVERY_KEY, {})
|
|
||||||
zha_const.populate_data()
|
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):
|
def device_joined(self, device):
|
||||||
"""Handle device joined.
|
"""Handle device joined.
|
||||||
|
|
||||||
@ -193,8 +262,11 @@ class ApplicationListener:
|
|||||||
component = None
|
component = None
|
||||||
profile_clusters = ([], [])
|
profile_clusters = ([], [])
|
||||||
device_key = "{}-{}".format(device.ieee, endpoint_id)
|
device_key = "{}-{}".format(device.ieee, endpoint_id)
|
||||||
node_config = self._config[DOMAIN][CONF_DEVICE_CONFIG].get(
|
node_config = {}
|
||||||
device_key, {})
|
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:
|
if endpoint.profile_id in zigpy.profiles.PROFILES:
|
||||||
profile = zigpy.profiles.PROFILES[endpoint.profile_id]
|
profile = zigpy.profiles.PROFILES[endpoint.profile_id]
|
||||||
@ -226,14 +298,16 @@ class ApplicationListener:
|
|||||||
'new_join': join,
|
'new_join': join,
|
||||||
'unique_id': device_key,
|
'unique_id': device_key,
|
||||||
}
|
}
|
||||||
self._hass.data[DISCOVERY_KEY][device_key] = discovery_info
|
|
||||||
|
|
||||||
await discovery.async_load_platform(
|
if join:
|
||||||
|
async_dispatcher_send(
|
||||||
self._hass,
|
self._hass,
|
||||||
component,
|
ZHA_DISCOVERY_NEW.format(component),
|
||||||
DOMAIN,
|
discovery_info
|
||||||
{'discovery_key': device_key},
|
)
|
||||||
self._config,
|
else:
|
||||||
|
self._hass.data[DATA_ZHA][component][device_key] = (
|
||||||
|
discovery_info
|
||||||
)
|
)
|
||||||
|
|
||||||
for cluster in endpoint.in_clusters.values():
|
for cluster in endpoint.in_clusters.values():
|
||||||
@ -309,12 +383,12 @@ class ApplicationListener:
|
|||||||
discovery_info[discovery_attr] = {cluster.cluster_id: cluster}
|
discovery_info[discovery_attr] = {cluster.cluster_id: cluster}
|
||||||
if sub_component:
|
if sub_component:
|
||||||
discovery_info.update({'sub_component': sub_component})
|
discovery_info.update({'sub_component': sub_component})
|
||||||
self._hass.data[DISCOVERY_KEY][cluster_key] = discovery_info
|
|
||||||
|
|
||||||
await discovery.async_load_platform(
|
if is_new_join:
|
||||||
|
async_dispatcher_send(
|
||||||
self._hass,
|
self._hass,
|
||||||
component,
|
ZHA_DISCOVERY_NEW.format(component),
|
||||||
DOMAIN,
|
discovery_info
|
||||||
{'discovery_key': cluster_key},
|
|
||||||
self._config,
|
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
self._hass.data[DATA_ZHA][component][cluster_key] = discovery_info
|
||||||
|
57
homeassistant/components/zha/config_flow.py
Normal file
57
homeassistant/components/zha/config_flow.py
Normal 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
|
||||||
|
)
|
@ -1,4 +1,51 @@
|
|||||||
"""All constants related to the ZHA component."""
|
"""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'
|
DISCOVERY_KEY = 'zha_discovery_info'
|
||||||
DEVICE_CLASS = {}
|
DEVICE_CLASS = {}
|
||||||
|
@ -5,28 +5,12 @@ For more details about this component, please refer to the documentation at
|
|||||||
https://home-assistant.io/components/zha/
|
https://home-assistant.io/components/zha/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import asyncio
|
||||||
|
from .const import RadioType, DEFAULT_BAUDRATE
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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):
|
async def safe_read(cluster, attributes, allow_cache=True, only_cache=False):
|
||||||
"""Swallow all exceptions from network read.
|
"""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",
|
"%s: failed to set reporting for '%s' attr on '%s' cluster: %s",
|
||||||
entity_id, attr_name, cluster_name, str(ex)
|
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
|
||||||
|
21
homeassistant/components/zha/strings.json
Normal file
21
homeassistant/components/zha/strings.json
Normal 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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -158,6 +158,7 @@ FLOWS = [
|
|||||||
'twilio',
|
'twilio',
|
||||||
'unifi',
|
'unifi',
|
||||||
'upnp',
|
'upnp',
|
||||||
|
'zha',
|
||||||
'zone',
|
'zone',
|
||||||
'zwave'
|
'zwave'
|
||||||
]
|
]
|
||||||
|
1
tests/components/zha/__init__.py
Normal file
1
tests/components/zha/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Tests for the ZHA component."""
|
77
tests/components/zha/test_config_flow.py
Normal file
77
tests/components/zha/test_config_flow.py
Normal 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'
|
Loading…
x
Reference in New Issue
Block a user