diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 8bfcbc705dc..2f88ad3a78b 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -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) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 8ee2c7850e3..4f1e24aad5b 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -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): diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index b00626031ed..695f2be5960 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -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(':')]) diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml index 0d7fe06fe25..048054077f8 100644 --- a/homeassistant/components/zha/services.yaml +++ b/homeassistant/components/zha/services.yaml @@ -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. diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py index d0763b8fb10..1d6b4fd3e01 100644 --- a/tests/components/zha/test_binary_sensor.py +++ b/tests/components/zha/test_binary_sensor.py @@ -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 diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index a70e0e5ea40..6f31f1bcad3 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -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 diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 0ccad52d6aa..e9d6370575b 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -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 diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index c348ef0d0a7..ec6af7f4aa1 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -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 diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index 1fc21e34cd8..b0bbc103a9e 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -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