mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
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:
parent
e26a5abb2b
commit
e670491c86
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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(':')])
|
||||
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user