Targeted ZHA permit joins. (#22482)

* Targeted ZHA permit service.

* Convert IEEE string to EUI64 usiv vol schema.

* Update test units.

* Lint.

isort imports.
This commit is contained in:
Alexei Chetroi 2019-03-27 22:50:52 -04:00 committed by Paulus Schoutsen
parent e26a5abb2b
commit e670491c86
9 changed files with 68 additions and 58 deletions

View File

@ -7,6 +7,7 @@ https://home-assistant.io/components/zha/
import asyncio
import logging
import voluptuous as vol
from homeassistant.components import websocket_api
@ -14,12 +15,14 @@ from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import async_get_registry
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .core.const import (
DOMAIN, ATTR_CLUSTER_ID, ATTR_CLUSTER_TYPE, ATTR_ATTRIBUTE, ATTR_VALUE,
ATTR_MANUFACTURER, ATTR_COMMAND, ATTR_COMMAND_TYPE, ATTR_ARGS, IN, OUT,
CLIENT_COMMANDS, SERVER_COMMANDS, SERVER, NAME, ATTR_ENDPOINT_ID,
DATA_ZHA_GATEWAY, DATA_ZHA, MFG_CLUSTER_ID_START)
from .core.helpers import get_matched_clusters, async_is_bindable_target
ATTR_ARGS, ATTR_ATTRIBUTE, ATTR_CLUSTER_ID, ATTR_CLUSTER_TYPE,
ATTR_COMMAND, ATTR_COMMAND_TYPE, ATTR_ENDPOINT_ID, ATTR_MANUFACTURER,
ATTR_VALUE, CLIENT_COMMANDS, DATA_ZHA, DATA_ZHA_GATEWAY, DOMAIN, IN,
MFG_CLUSTER_ID_START, NAME, OUT, SERVER, SERVER_COMMANDS)
from .core.helpers import (
async_is_bindable_target, convert_ieee, get_matched_clusters)
_LOGGER = logging.getLogger(__name__)
@ -48,14 +51,15 @@ IEEE_SERVICE = 'ieee_based_service'
SERVICE_SCHEMAS = {
SERVICE_PERMIT: vol.Schema({
vol.Optional(ATTR_IEEE_ADDRESS, default=None): convert_ieee,
vol.Optional(ATTR_DURATION, default=60):
vol.All(vol.Coerce(int), vol.Range(1, 254)),
vol.All(vol.Coerce(int), vol.Range(0, 254)),
}),
IEEE_SERVICE: vol.Schema({
vol.Required(ATTR_IEEE_ADDRESS): cv.string,
vol.Required(ATTR_IEEE_ADDRESS): convert_ieee,
}),
SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE: vol.Schema({
vol.Required(ATTR_IEEE): cv.string,
vol.Required(ATTR_IEEE): convert_ieee,
vol.Required(ATTR_ENDPOINT_ID): cv.positive_int,
vol.Required(ATTR_CLUSTER_ID): cv.positive_int,
vol.Optional(ATTR_CLUSTER_TYPE, default=IN): cv.string,
@ -64,7 +68,7 @@ SERVICE_SCHEMAS = {
vol.Optional(ATTR_MANUFACTURER): cv.positive_int,
}),
SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND: vol.Schema({
vol.Required(ATTR_IEEE): cv.string,
vol.Required(ATTR_IEEE): convert_ieee,
vol.Required(ATTR_ENDPOINT_ID): cv.positive_int,
vol.Required(ATTR_CLUSTER_ID): cv.positive_int,
vol.Optional(ATTR_CLUSTER_TYPE, default=IN): cv.string,
@ -79,11 +83,16 @@ SERVICE_SCHEMAS = {
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command({
vol.Required('type'): 'zha/devices/permit'
vol.Required('type'): 'zha/devices/permit',
vol.Optional(ATTR_IEEE, default=None): convert_ieee,
vol.Optional(ATTR_DURATION, default=60): vol.All(vol.Coerce(int),
vol.Range(0, 254))
})
async def websocket_permit_devices(hass, connection, msg):
"""Permit ZHA zigbee devices."""
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
duration = msg.get(ATTR_DURATION)
ieee = msg.get(ATTR_IEEE)
async def forward_messages(data):
"""Forward events to websocket."""
@ -103,8 +112,8 @@ async def websocket_permit_devices(hass, connection, msg):
connection.subscriptions[msg['id']] = async_cleanup
zha_gateway.async_enable_debug_mode()
await zha_gateway.application_controller.permit(60)
await zha_gateway.application_controller.permit(time_s=duration,
node=ieee)
connection.send_result(msg['id'])
@ -153,7 +162,7 @@ def async_get_device_info(hass, device, ha_device_registry=None):
@websocket_api.async_response
@websocket_api.websocket_command({
vol.Required(TYPE): 'zha/devices/reconfigure',
vol.Required(ATTR_IEEE): str
vol.Required(ATTR_IEEE): convert_ieee,
})
async def websocket_reconfigure_node(hass, connection, msg):
"""Reconfigure a ZHA nodes entities by its ieee address."""
@ -168,7 +177,7 @@ async def websocket_reconfigure_node(hass, connection, msg):
@websocket_api.async_response
@websocket_api.websocket_command({
vol.Required(TYPE): 'zha/devices/clusters',
vol.Required(ATTR_IEEE): str
vol.Required(ATTR_IEEE): convert_ieee,
})
async def websocket_device_clusters(hass, connection, msg):
"""Return a list of device clusters."""
@ -201,7 +210,7 @@ async def websocket_device_clusters(hass, connection, msg):
@websocket_api.async_response
@websocket_api.websocket_command({
vol.Required(TYPE): 'zha/devices/clusters/attributes',
vol.Required(ATTR_IEEE): str,
vol.Required(ATTR_IEEE): convert_ieee,
vol.Required(ATTR_ENDPOINT_ID): int,
vol.Required(ATTR_CLUSTER_ID): int,
vol.Required(ATTR_CLUSTER_TYPE): str
@ -243,7 +252,7 @@ async def websocket_device_cluster_attributes(hass, connection, msg):
@websocket_api.async_response
@websocket_api.websocket_command({
vol.Required(TYPE): 'zha/devices/clusters/commands',
vol.Required(ATTR_IEEE): str,
vol.Required(ATTR_IEEE): convert_ieee,
vol.Required(ATTR_ENDPOINT_ID): int,
vol.Required(ATTR_CLUSTER_ID): int,
vol.Required(ATTR_CLUSTER_TYPE): str
@ -295,7 +304,7 @@ async def websocket_device_cluster_commands(hass, connection, msg):
@websocket_api.async_response
@websocket_api.websocket_command({
vol.Required(TYPE): 'zha/devices/clusters/attributes/value',
vol.Required(ATTR_IEEE): str,
vol.Required(ATTR_IEEE): convert_ieee,
vol.Required(ATTR_ENDPOINT_ID): int,
vol.Required(ATTR_CLUSTER_ID): int,
vol.Required(ATTR_CLUSTER_TYPE): str,
@ -340,7 +349,7 @@ async def websocket_read_zigbee_cluster_attributes(hass, connection, msg):
@websocket_api.async_response
@websocket_api.websocket_command({
vol.Required(TYPE): 'zha/devices/bindable',
vol.Required(ATTR_IEEE): str,
vol.Required(ATTR_IEEE): convert_ieee,
})
async def websocket_get_bindable_devices(hass, connection, msg):
"""Directly bind devices."""
@ -369,8 +378,8 @@ async def websocket_get_bindable_devices(hass, connection, msg):
@websocket_api.async_response
@websocket_api.websocket_command({
vol.Required(TYPE): 'zha/devices/bind',
vol.Required(ATTR_SOURCE_IEEE): str,
vol.Required(ATTR_TARGET_IEEE): str
vol.Required(ATTR_SOURCE_IEEE): convert_ieee,
vol.Required(ATTR_TARGET_IEEE): convert_ieee,
})
async def websocket_bind_devices(hass, connection, msg):
"""Directly bind devices."""
@ -389,8 +398,8 @@ async def websocket_bind_devices(hass, connection, msg):
@websocket_api.async_response
@websocket_api.websocket_command({
vol.Required(TYPE): 'zha/devices/unbind',
vol.Required(ATTR_SOURCE_IEEE): str,
vol.Required(ATTR_TARGET_IEEE): str
vol.Required(ATTR_SOURCE_IEEE): convert_ieee,
vol.Required(ATTR_TARGET_IEEE): convert_ieee,
})
async def websocket_unbind_devices(hass, connection, msg):
"""Remove a direct binding between devices."""
@ -450,17 +459,20 @@ def async_load_api(hass):
async def permit(service):
"""Allow devices to join this network."""
duration = service.data.get(ATTR_DURATION)
_LOGGER.info("Permitting joins for %ss", duration)
await application_controller.permit(duration)
ieee = service.data.get(ATTR_IEEE_ADDRESS)
if ieee:
_LOGGER.info("Permitting joins for %ss on %s device",
duration, ieee)
else:
_LOGGER.info("Permitting joins for %ss", duration)
await application_controller.permit(time_s=duration, node=ieee)
hass.helpers.service.async_register_admin_service(
DOMAIN, SERVICE_PERMIT, permit, schema=SERVICE_SCHEMAS[SERVICE_PERMIT])
async def remove(service):
"""Remove a node from the network."""
from bellows.types import EmberEUI64, uint8_t
ieee = service.data.get(ATTR_IEEE_ADDRESS)
ieee = EmberEUI64([uint8_t(p, base=16) for p in ieee.split(':')])
_LOGGER.info("Removing node %s", ieee)
await application_controller.remove(ieee)

View File

@ -10,35 +10,31 @@ import collections
import itertools
import logging
import os
import traceback
from homeassistant.components.system_log import LogEntry, _figure_out_source
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity_component import EntityComponent
from ..api import async_get_device_info
from .channels import MAINS_POWERED, ZDOChannel
from .const import (
DATA_ZHA, DATA_ZHA_CORE_COMPONENT, DOMAIN, SIGNAL_REMOVE, DATA_ZHA_GATEWAY,
CONF_USB_PATH, CONF_BAUDRATE, DEFAULT_BAUDRATE, CONF_RADIO_TYPE,
DATA_ZHA_RADIO, CONF_DATABASE, DEFAULT_DATABASE_NAME, DATA_ZHA_BRIDGE_ID,
RADIO, CONTROLLER, RADIO_DESCRIPTION, BELLOWS, ZHA, ZIGPY, ZIGPY_XBEE,
ZIGPY_DECONZ, ORIGINAL, CURRENT, DEBUG_LEVELS, ADD_DEVICE_RELAY_LOGGERS,
TYPE, NWK, IEEE, MODEL, SIGNATURE, ATTR_MANUFACTURER, RAW_INIT,
ZHA_GW_MSG, DEVICE_REMOVED, DEVICE_INFO, DEVICE_FULL_INIT, DEVICE_JOINED,
LOG_OUTPUT, LOG_ENTRY
)
from .device import ZHADevice, DeviceStatus
from .channels import (
ZDOChannel, MAINS_POWERED
)
from .helpers import convert_ieee
ADD_DEVICE_RELAY_LOGGERS, ATTR_MANUFACTURER, BELLOWS, CONF_BAUDRATE,
CONF_DATABASE, CONF_RADIO_TYPE, CONF_USB_PATH, CONTROLLER, CURRENT,
DATA_ZHA, DATA_ZHA_BRIDGE_ID, DATA_ZHA_CORE_COMPONENT, DATA_ZHA_GATEWAY,
DATA_ZHA_RADIO, DEBUG_LEVELS, DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME,
DEVICE_FULL_INIT, DEVICE_INFO, DEVICE_JOINED, DEVICE_REMOVED, DOMAIN, IEEE,
LOG_ENTRY, LOG_OUTPUT, MODEL, NWK, ORIGINAL, RADIO, RADIO_DESCRIPTION,
RAW_INIT, SIGNAL_REMOVE, SIGNATURE, TYPE, ZHA, ZHA_GW_MSG, ZIGPY,
ZIGPY_DECONZ, ZIGPY_XBEE)
from .device import DeviceStatus, ZHADevice
from .discovery import (
async_process_endpoint, async_dispatch_discovery_info,
async_create_device_entity
)
from .store import async_get_registry
async_create_device_entity, async_dispatch_discovery_info,
async_process_endpoint)
from .patches import apply_application_controller_patch
from .registries import RADIO_TYPES
from ..api import async_get_device_info
from .store import async_get_registry
_LOGGER = logging.getLogger(__name__)
@ -169,9 +165,8 @@ class ZHAGateway:
}
)
def get_device(self, ieee_str):
def get_device(self, ieee):
"""Return ZHADevice for given ieee."""
ieee = convert_ieee(ieee_str)
return self._devices.get(ieee)
def get_entity_reference(self, entity_id):

View File

@ -148,6 +148,8 @@ async def check_zigpy_connection(usb_path, radio_type, database_path):
def convert_ieee(ieee_str):
"""Convert given ieee string to EUI64."""
from zigpy.types import EUI64, uint8_t
if ieee_str is None:
return None
return EUI64([uint8_t(p, base=16) for p in ieee_str.split(':')])

View File

@ -6,6 +6,9 @@ permit:
duration:
description: Time to permit joins, in seconds
example: 60
ieee_address:
description: IEEE address of the node permitting new joins
example: "00:0d:6f:00:05:7d:2d:34"
remove:
description: Remove a node from the ZigBee network.

View File

@ -54,15 +54,14 @@ async def test_binary_sensor(hass, config_entry, zha_gateway):
zone_cluster = zigpy_device_zone.endpoints.get(
1).ias_zone
zone_entity_id = make_entity_id(DOMAIN, zigpy_device_zone, zone_cluster)
zone_zha_device = zha_gateway.get_device(str(zigpy_device_zone.ieee))
zone_zha_device = zha_gateway.get_device(zigpy_device_zone.ieee)
# occupancy binary_sensor
occupancy_cluster = zigpy_device_occupancy.endpoints.get(
1).occupancy
occupancy_entity_id = make_entity_id(
DOMAIN, zigpy_device_occupancy, occupancy_cluster)
occupancy_zha_device = zha_gateway.get_device(
str(zigpy_device_occupancy.ieee))
occupancy_zha_device = zha_gateway.get_device(zigpy_device_occupancy.ieee)
# dimmable binary_sensor
remote_on_off_cluster = zigpy_device_remote.endpoints.get(
@ -72,7 +71,7 @@ async def test_binary_sensor(hass, config_entry, zha_gateway):
remote_entity_id = make_entity_id(DOMAIN, zigpy_device_remote,
remote_on_off_cluster,
use_suffix=False)
remote_zha_device = zha_gateway.get_device(str(zigpy_device_remote.ieee))
remote_zha_device = zha_gateway.get_device(zigpy_device_remote.ieee)
# test that the sensors exist and are in the unavailable state
assert hass.states.get(zone_entity_id).state == STATE_UNAVAILABLE

View File

@ -31,7 +31,7 @@ async def test_fan(hass, config_entry, zha_gateway):
cluster = zigpy_device.endpoints.get(1).fan
entity_id = make_entity_id(DOMAIN, zigpy_device, cluster)
zha_device = zha_gateway.get_device(str(zigpy_device.ieee))
zha_device = zha_gateway.get_device(zigpy_device.ieee)
# test that the fan was created and that it is unavailable
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE

View File

@ -51,7 +51,7 @@ async def test_light(hass, config_entry, zha_gateway, monkeypatch):
on_off_entity_id = make_entity_id(DOMAIN, zigpy_device_on_off,
on_off_device_on_off_cluster,
use_suffix=False)
on_off_zha_device = zha_gateway.get_device(str(zigpy_device_on_off.ieee))
on_off_zha_device = zha_gateway.get_device(zigpy_device_on_off.ieee)
# dimmable light
level_device_on_off_cluster = zigpy_device_level.endpoints.get(1).on_off
@ -65,7 +65,7 @@ async def test_light(hass, config_entry, zha_gateway, monkeypatch):
level_entity_id = make_entity_id(DOMAIN, zigpy_device_level,
level_device_on_off_cluster,
use_suffix=False)
level_zha_device = zha_gateway.get_device(str(zigpy_device_level.ieee))
level_zha_device = zha_gateway.get_device(zigpy_device_level.ieee)
# test that the lights were created and that they are unavailable
assert hass.states.get(on_off_entity_id).state == STATE_UNAVAILABLE

View File

@ -114,8 +114,7 @@ async def async_build_devices(hass, zha_gateway, config_entry, cluster_ids):
1).in_clusters[cluster_id]
device_info["entity_id"] = make_entity_id(
DOMAIN, zigpy_device, device_info["cluster"])
device_info["zha_device"] = zha_gateway.get_device(
str(zigpy_device.ieee))
device_info["zha_device"] = zha_gateway.get_device(zigpy_device.ieee)
return device_infos

View File

@ -28,7 +28,7 @@ async def test_switch(hass, config_entry, zha_gateway):
cluster = zigpy_device.endpoints.get(1).on_off
entity_id = make_entity_id(DOMAIN, zigpy_device, cluster)
zha_device = zha_gateway.get_device(str(zigpy_device.ieee))
zha_device = zha_gateway.get_device(zigpy_device.ieee)
# test that the switch was created and that its state is unavailable
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE