Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Targeted ZHA permit joins. #22482

Merged
merged 4 commits into from Mar 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
64 changes: 38 additions & 26 deletions homeassistant/components/zha/api.py
Expand Up @@ -7,19 +7,22 @@

import asyncio
import logging

import voluptuous as vol

from homeassistant.components import websocket_api
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__)

Expand Down Expand Up @@ -48,14 +51,15 @@

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,
Expand All @@ -64,7 +68,7 @@
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,
Expand All @@ -79,11 +83,16 @@
@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."""
Expand All @@ -103,8 +112,8 @@ def async_cleanup() -> None:

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'])


Expand Down Expand Up @@ -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."""
Expand All @@ -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."""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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."""
Expand All @@ -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."""
Expand Down Expand Up @@ -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)

Expand Down
39 changes: 17 additions & 22 deletions homeassistant/components/zha/core/gateway.py
Expand Up @@ -10,35 +10,31 @@
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__)

Expand Down Expand Up @@ -169,9 +165,8 @@ def device_removed(self, device):
}
)

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):
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/zha/core/helpers.py
Expand Up @@ -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(':')])


Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/zha/services.yaml
Expand Up @@ -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.
Expand Down
7 changes: 3 additions & 4 deletions tests/components/zha/test_binary_sensor.py
Expand Up @@ -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(
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/components/zha/test_fan.py
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions tests/components/zha/test_light.py
Expand Up @@ -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
Expand All @@ -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
Expand Down
3 changes: 1 addition & 2 deletions tests/components/zha/test_sensor.py
Expand Up @@ -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


Expand Down
2 changes: 1 addition & 1 deletion tests/components/zha/test_switch.py
Expand Up @@ -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
Expand Down