Refactor permit services to allow joining using install codes (#40652)
* Update zha.permit schema to support install code * Move install code to core helpers * QR code converter for enbrighten * Fix schemas * Update test for permit service * Refactor zha.permit to accept install codes * Test zha.permit from QR code * Fix regex for Embrighten QR code * Add regex for Aqara QR codes * Add Consciot regex for QR code * Reuse test params for WS tests * ZHA WS permit command with install code * Tests for zha.permit WS service * Refactor zha.permit and zha.remove service to use ATTR_IEEE for the address * Make pylint happy * Deprecate only ieee_address param for now
This commit is contained in:
parent
b9823791f7
commit
47286fbe2a
4 changed files with 400 additions and 23 deletions
|
@ -23,6 +23,7 @@ from .core.const import (
|
||||||
ATTR_COMMAND,
|
ATTR_COMMAND,
|
||||||
ATTR_COMMAND_TYPE,
|
ATTR_COMMAND_TYPE,
|
||||||
ATTR_ENDPOINT_ID,
|
ATTR_ENDPOINT_ID,
|
||||||
|
ATTR_IEEE,
|
||||||
ATTR_LEVEL,
|
ATTR_LEVEL,
|
||||||
ATTR_MANUFACTURER,
|
ATTR_MANUFACTURER,
|
||||||
ATTR_MEMBERS,
|
ATTR_MEMBERS,
|
||||||
|
@ -54,7 +55,12 @@ from .core.const import (
|
||||||
WARNING_DEVICE_STROBE_YES,
|
WARNING_DEVICE_STROBE_YES,
|
||||||
)
|
)
|
||||||
from .core.group import GroupMember
|
from .core.group import GroupMember
|
||||||
from .core.helpers import async_is_bindable_target, get_matched_clusters
|
from .core.helpers import (
|
||||||
|
async_is_bindable_target,
|
||||||
|
convert_install_code,
|
||||||
|
get_matched_clusters,
|
||||||
|
qr_to_install_code,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -67,9 +73,10 @@ DEVICE_INFO = "device_info"
|
||||||
ATTR_DURATION = "duration"
|
ATTR_DURATION = "duration"
|
||||||
ATTR_GROUP = "group"
|
ATTR_GROUP = "group"
|
||||||
ATTR_IEEE_ADDRESS = "ieee_address"
|
ATTR_IEEE_ADDRESS = "ieee_address"
|
||||||
ATTR_IEEE = "ieee"
|
ATTR_INSTALL_CODE = "install_code"
|
||||||
ATTR_SOURCE_IEEE = "source_ieee"
|
ATTR_SOURCE_IEEE = "source_ieee"
|
||||||
ATTR_TARGET_IEEE = "target_ieee"
|
ATTR_TARGET_IEEE = "target_ieee"
|
||||||
|
ATTR_QR_CODE = "qr_code"
|
||||||
|
|
||||||
SERVICE_PERMIT = "permit"
|
SERVICE_PERMIT = "permit"
|
||||||
SERVICE_REMOVE = "remove"
|
SERVICE_REMOVE = "remove"
|
||||||
|
@ -83,16 +90,29 @@ SERVICE_WARNING_DEVICE_WARN = "warning_device_warn"
|
||||||
SERVICE_ZIGBEE_BIND = "service_zigbee_bind"
|
SERVICE_ZIGBEE_BIND = "service_zigbee_bind"
|
||||||
IEEE_SERVICE = "ieee_based_service"
|
IEEE_SERVICE = "ieee_based_service"
|
||||||
|
|
||||||
|
SERVICE_PERMIT_PARAMS = {
|
||||||
|
vol.Optional(ATTR_IEEE, default=None): EUI64.convert,
|
||||||
|
vol.Optional(ATTR_DURATION, default=60): vol.All(
|
||||||
|
vol.Coerce(int), vol.Range(0, 254)
|
||||||
|
),
|
||||||
|
vol.Inclusive(ATTR_SOURCE_IEEE, "install_code"): EUI64.convert,
|
||||||
|
vol.Inclusive(ATTR_INSTALL_CODE, "install_code"): convert_install_code,
|
||||||
|
vol.Exclusive(ATTR_QR_CODE, "install_code"): vol.All(str, qr_to_install_code),
|
||||||
|
}
|
||||||
|
|
||||||
SERVICE_SCHEMAS = {
|
SERVICE_SCHEMAS = {
|
||||||
SERVICE_PERMIT: vol.Schema(
|
SERVICE_PERMIT: vol.Schema(
|
||||||
{
|
vol.All(
|
||||||
vol.Optional(ATTR_IEEE_ADDRESS, default=None): EUI64.convert,
|
cv.deprecated(ATTR_IEEE_ADDRESS, replacement_key=ATTR_IEEE),
|
||||||
vol.Optional(ATTR_DURATION, default=60): vol.All(
|
SERVICE_PERMIT_PARAMS,
|
||||||
vol.Coerce(int), vol.Range(0, 254)
|
)
|
||||||
),
|
),
|
||||||
}
|
IEEE_SERVICE: vol.Schema(
|
||||||
|
vol.All(
|
||||||
|
cv.deprecated(ATTR_IEEE_ADDRESS, replacement_key=ATTR_IEEE),
|
||||||
|
{vol.Required(ATTR_IEEE): EUI64.convert},
|
||||||
|
)
|
||||||
),
|
),
|
||||||
IEEE_SERVICE: vol.Schema({vol.Required(ATTR_IEEE_ADDRESS): EUI64.convert}),
|
|
||||||
SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE: vol.Schema(
|
SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE: vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(ATTR_IEEE): EUI64.convert,
|
vol.Required(ATTR_IEEE): EUI64.convert,
|
||||||
|
@ -169,13 +189,7 @@ ClusterBinding = collections.namedtuple("ClusterBinding", "id endpoint_id type n
|
||||||
@websocket_api.require_admin
|
@websocket_api.require_admin
|
||||||
@websocket_api.async_response
|
@websocket_api.async_response
|
||||||
@websocket_api.websocket_command(
|
@websocket_api.websocket_command(
|
||||||
{
|
{vol.Required("type"): "zha/devices/permit", **SERVICE_PERMIT_PARAMS}
|
||||||
vol.Required("type"): "zha/devices/permit",
|
|
||||||
vol.Optional(ATTR_IEEE, default=None): EUI64.convert,
|
|
||||||
vol.Optional(ATTR_DURATION, default=60): vol.All(
|
|
||||||
vol.Coerce(int), vol.Range(0, 254)
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
async def websocket_permit_devices(hass, connection, msg):
|
async def websocket_permit_devices(hass, connection, msg):
|
||||||
"""Permit ZHA zigbee devices."""
|
"""Permit ZHA zigbee devices."""
|
||||||
|
@ -199,7 +213,21 @@ async def websocket_permit_devices(hass, connection, msg):
|
||||||
|
|
||||||
connection.subscriptions[msg["id"]] = async_cleanup
|
connection.subscriptions[msg["id"]] = async_cleanup
|
||||||
zha_gateway.async_enable_debug_mode()
|
zha_gateway.async_enable_debug_mode()
|
||||||
await zha_gateway.application_controller.permit(time_s=duration, node=ieee)
|
if ATTR_SOURCE_IEEE in msg:
|
||||||
|
src_ieee = msg[ATTR_SOURCE_IEEE]
|
||||||
|
code = msg[ATTR_INSTALL_CODE]
|
||||||
|
_LOGGER.debug("Allowing join for %s device with install code", src_ieee)
|
||||||
|
await zha_gateway.application_controller.permit_with_key(
|
||||||
|
time_s=duration, node=src_ieee, code=code
|
||||||
|
)
|
||||||
|
elif ATTR_QR_CODE in msg:
|
||||||
|
src_ieee, code = msg[ATTR_QR_CODE]
|
||||||
|
_LOGGER.debug("Allowing join for %s device with install code", src_ieee)
|
||||||
|
await zha_gateway.application_controller.permit_with_key(
|
||||||
|
time_s=duration, node=src_ieee, code=code
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await zha_gateway.application_controller.permit(time_s=duration, node=ieee)
|
||||||
connection.send_result(msg["id"])
|
connection.send_result(msg["id"])
|
||||||
|
|
||||||
|
|
||||||
|
@ -826,8 +854,25 @@ def async_load_api(hass):
|
||||||
|
|
||||||
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[ATTR_DURATION]
|
||||||
ieee = service.data.get(ATTR_IEEE_ADDRESS)
|
ieee = service.data.get(ATTR_IEEE)
|
||||||
|
if ATTR_SOURCE_IEEE in service.data:
|
||||||
|
src_ieee = service.data[ATTR_SOURCE_IEEE]
|
||||||
|
code = service.data[ATTR_INSTALL_CODE]
|
||||||
|
_LOGGER.info("Allowing join for %s device with install code", src_ieee)
|
||||||
|
await application_controller.permit_with_key(
|
||||||
|
time_s=duration, node=src_ieee, code=code
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if ATTR_QR_CODE in service.data:
|
||||||
|
src_ieee, code = service.data[ATTR_QR_CODE]
|
||||||
|
_LOGGER.info("Allowing join for %s device with install code", src_ieee)
|
||||||
|
await application_controller.permit_with_key(
|
||||||
|
time_s=duration, node=src_ieee, code=code
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
if ieee:
|
if ieee:
|
||||||
_LOGGER.info("Permitting joins for %ss on %s device", duration, ieee)
|
_LOGGER.info("Permitting joins for %ss on %s device", duration, ieee)
|
||||||
else:
|
else:
|
||||||
|
@ -840,7 +885,7 @@ def async_load_api(hass):
|
||||||
|
|
||||||
async def remove(service):
|
async def remove(service):
|
||||||
"""Remove a node from the network."""
|
"""Remove a node from the network."""
|
||||||
ieee = service.data[ATTR_IEEE_ADDRESS]
|
ieee = service.data[ATTR_IEEE]
|
||||||
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
|
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
|
||||||
zha_device = zha_gateway.get_device(ieee)
|
zha_device = zha_gateway.get_device(ieee)
|
||||||
if zha_device is not None and zha_device.is_coordinator:
|
if zha_device is not None and zha_device.is_coordinator:
|
||||||
|
|
|
@ -6,15 +6,19 @@ https://home-assistant.io/integrations/zha/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import binascii
|
||||||
import collections
|
import collections
|
||||||
import functools
|
import functools
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
from random import uniform
|
from random import uniform
|
||||||
from typing import Any, Callable, Iterator, List, Optional
|
import re
|
||||||
|
from typing import Any, Callable, Iterator, List, Optional, Tuple
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
import zigpy.exceptions
|
import zigpy.exceptions
|
||||||
import zigpy.types
|
import zigpy.types
|
||||||
|
import zigpy.util
|
||||||
|
|
||||||
from homeassistant.core import State, callback
|
from homeassistant.core import State, callback
|
||||||
|
|
||||||
|
@ -205,3 +209,63 @@ def retryable_req(
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def convert_install_code(value: str) -> bytes:
|
||||||
|
"""Convert string to install code bytes and validate length."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
code = binascii.unhexlify(value.replace("-", "").lower())
|
||||||
|
except binascii.Error as exc:
|
||||||
|
raise vol.Invalid(f"invalid hex string: {value}") from exc
|
||||||
|
|
||||||
|
if len(code) != 18: # 16 byte code + 2 crc bytes
|
||||||
|
raise vol.Invalid("invalid length of the install code")
|
||||||
|
|
||||||
|
if zigpy.util.convert_install_code(code) is None:
|
||||||
|
raise vol.Invalid("invalid install code")
|
||||||
|
|
||||||
|
return code
|
||||||
|
|
||||||
|
|
||||||
|
QR_CODES = (
|
||||||
|
# Consciot
|
||||||
|
r"^([\da-fA-F]{16})\|([\da-fA-F]{36})$",
|
||||||
|
# Enbrighten
|
||||||
|
r"""
|
||||||
|
^Z:
|
||||||
|
([0-9a-fA-F]{16}) # IEEE address
|
||||||
|
\$I:
|
||||||
|
([0-9a-fA-F]{36}) # install code
|
||||||
|
$
|
||||||
|
""",
|
||||||
|
# Aqara
|
||||||
|
r"""
|
||||||
|
\$A:
|
||||||
|
([0-9a-fA-F]{16}) # IEEE address
|
||||||
|
\$I:
|
||||||
|
([0-9a-fA-F]{36}) # install code
|
||||||
|
$
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def qr_to_install_code(qr_code: str) -> Tuple[zigpy.types.EUI64, bytes]:
|
||||||
|
"""Try to parse the QR code.
|
||||||
|
|
||||||
|
if successful, return a tuple of a EUI64 address and install code.
|
||||||
|
"""
|
||||||
|
|
||||||
|
for code_pattern in QR_CODES:
|
||||||
|
match = re.search(code_pattern, qr_code, re.VERBOSE)
|
||||||
|
if match is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ieee_hex = binascii.unhexlify(match[1])
|
||||||
|
ieee = zigpy.types.EUI64(ieee_hex[::-1])
|
||||||
|
install_code = match[2]
|
||||||
|
# install_code sanity check
|
||||||
|
install_code = convert_install_code(install_code)
|
||||||
|
return ieee, install_code
|
||||||
|
|
||||||
|
raise vol.Invalid(f"couldn't convert qr code: {qr_code}")
|
||||||
|
|
|
@ -9,6 +9,15 @@ permit:
|
||||||
ieee_address:
|
ieee_address:
|
||||||
description: IEEE address of the node permitting new joins
|
description: IEEE address of the node permitting new joins
|
||||||
example: "00:0d:6f:00:05:7d:2d:34"
|
example: "00:0d:6f:00:05:7d:2d:34"
|
||||||
|
source_ieee:
|
||||||
|
description: IEEE address of the joining device (must be used with install code)
|
||||||
|
example: "00:0a:bf:00:01:10:23:35"
|
||||||
|
install_code:
|
||||||
|
description: Install code of the joining device (must be used with source_ieee)
|
||||||
|
example: "1234-5678-1234-5678-AABB-CCDD-AABB-CCDD-EEFF"
|
||||||
|
qr_code:
|
||||||
|
description: value of the QR install code (different between vendors)
|
||||||
|
example: "Z:000D6FFFFED4163B$I:52797BF4A5084DAA8E1712B61741CA024051"
|
||||||
|
|
||||||
remove:
|
remove:
|
||||||
description: Remove a node from the Zigbee network.
|
description: Remove a node from the Zigbee network.
|
||||||
|
|
|
@ -1,11 +1,24 @@
|
||||||
"""Test ZHA API."""
|
"""Test ZHA API."""
|
||||||
|
from binascii import unhexlify
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import voluptuous as vol
|
||||||
import zigpy.profiles.zha
|
import zigpy.profiles.zha
|
||||||
|
import zigpy.types
|
||||||
import zigpy.zcl.clusters.general as general
|
import zigpy.zcl.clusters.general as general
|
||||||
|
|
||||||
from homeassistant.components.websocket_api import const
|
from homeassistant.components.websocket_api import const
|
||||||
from homeassistant.components.zha.api import ID, TYPE, async_load_api
|
from homeassistant.components.zha import DOMAIN
|
||||||
|
from homeassistant.components.zha.api import (
|
||||||
|
ATTR_DURATION,
|
||||||
|
ATTR_INSTALL_CODE,
|
||||||
|
ATTR_QR_CODE,
|
||||||
|
ATTR_SOURCE_IEEE,
|
||||||
|
ID,
|
||||||
|
SERVICE_PERMIT,
|
||||||
|
TYPE,
|
||||||
|
async_load_api,
|
||||||
|
)
|
||||||
from homeassistant.components.zha.core.const import (
|
from homeassistant.components.zha.core.const import (
|
||||||
ATTR_CLUSTER_ID,
|
ATTR_CLUSTER_ID,
|
||||||
ATTR_CLUSTER_TYPE,
|
ATTR_CLUSTER_TYPE,
|
||||||
|
@ -16,13 +29,18 @@ from homeassistant.components.zha.core.const import (
|
||||||
ATTR_NAME,
|
ATTR_NAME,
|
||||||
ATTR_QUIRK_APPLIED,
|
ATTR_QUIRK_APPLIED,
|
||||||
CLUSTER_TYPE_IN,
|
CLUSTER_TYPE_IN,
|
||||||
|
DATA_ZHA,
|
||||||
|
DATA_ZHA_GATEWAY,
|
||||||
GROUP_ID,
|
GROUP_ID,
|
||||||
GROUP_IDS,
|
GROUP_IDS,
|
||||||
GROUP_NAME,
|
GROUP_NAME,
|
||||||
)
|
)
|
||||||
|
from homeassistant.core import Context
|
||||||
|
|
||||||
from .conftest import FIXTURE_GRP_ID, FIXTURE_GRP_NAME
|
from .conftest import FIXTURE_GRP_ID, FIXTURE_GRP_NAME
|
||||||
|
|
||||||
|
from tests.async_mock import AsyncMock, patch
|
||||||
|
|
||||||
IEEE_SWITCH_DEVICE = "01:2d:6f:00:0a:90:69:e7"
|
IEEE_SWITCH_DEVICE = "01:2d:6f:00:0a:90:69:e7"
|
||||||
IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8"
|
IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8"
|
||||||
|
|
||||||
|
@ -225,7 +243,7 @@ async def test_get_group(zha_client):
|
||||||
|
|
||||||
async def test_get_group_not_found(zha_client):
|
async def test_get_group_not_found(zha_client):
|
||||||
"""Test not found response from get group API."""
|
"""Test not found response from get group API."""
|
||||||
await zha_client.send_json({ID: 9, TYPE: "zha/group", GROUP_ID: 1234567})
|
await zha_client.send_json({ID: 9, TYPE: "zha/group", GROUP_ID: 1_234_567})
|
||||||
|
|
||||||
msg = await zha_client.receive_json()
|
msg = await zha_client.receive_json()
|
||||||
|
|
||||||
|
@ -335,3 +353,244 @@ async def test_remove_group(zha_client):
|
||||||
|
|
||||||
groups = msg["result"]
|
groups = msg["result"]
|
||||||
assert len(groups) == 0
|
assert len(groups) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def app_controller(hass, setup_zha):
|
||||||
|
"""Fixture for zigpy Application Controller."""
|
||||||
|
await setup_zha()
|
||||||
|
controller = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY].application_controller
|
||||||
|
p1 = patch.object(controller, "permit")
|
||||||
|
p2 = patch.object(controller, "permit_with_key", new=AsyncMock())
|
||||||
|
with p1, p2:
|
||||||
|
yield controller
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"params, duration, node",
|
||||||
|
(
|
||||||
|
({}, 60, None),
|
||||||
|
({ATTR_DURATION: 30}, 30, None),
|
||||||
|
(
|
||||||
|
{ATTR_DURATION: 33, ATTR_IEEE: "aa:bb:cc:dd:aa:bb:cc:dd"},
|
||||||
|
33,
|
||||||
|
zigpy.types.EUI64.convert("aa:bb:cc:dd:aa:bb:cc:dd"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{ATTR_IEEE: "aa:bb:cc:dd:aa:bb:cc:d1"},
|
||||||
|
60,
|
||||||
|
zigpy.types.EUI64.convert("aa:bb:cc:dd:aa:bb:cc:d1"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def test_permit_ha12(
|
||||||
|
hass, app_controller, hass_admin_user, params, duration, node
|
||||||
|
):
|
||||||
|
"""Test permit service."""
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN, SERVICE_PERMIT, params, True, Context(user_id=hass_admin_user.id)
|
||||||
|
)
|
||||||
|
assert app_controller.permit.await_count == 1
|
||||||
|
assert app_controller.permit.await_args[1]["time_s"] == duration
|
||||||
|
assert app_controller.permit.await_args[1]["node"] == node
|
||||||
|
assert app_controller.permit_with_key.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
IC_TEST_PARAMS = (
|
||||||
|
(
|
||||||
|
{
|
||||||
|
ATTR_SOURCE_IEEE: IEEE_SWITCH_DEVICE,
|
||||||
|
ATTR_INSTALL_CODE: "5279-7BF4-A508-4DAA-8E17-12B6-1741-CA02-4051",
|
||||||
|
},
|
||||||
|
zigpy.types.EUI64.convert(IEEE_SWITCH_DEVICE),
|
||||||
|
unhexlify("52797BF4A5084DAA8E1712B61741CA024051"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
ATTR_SOURCE_IEEE: IEEE_SWITCH_DEVICE,
|
||||||
|
ATTR_INSTALL_CODE: "52797BF4A5084DAA8E1712B61741CA024051",
|
||||||
|
},
|
||||||
|
zigpy.types.EUI64.convert(IEEE_SWITCH_DEVICE),
|
||||||
|
unhexlify("52797BF4A5084DAA8E1712B61741CA024051"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("params, src_ieee, code", IC_TEST_PARAMS)
|
||||||
|
async def test_permit_with_install_code(
|
||||||
|
hass, app_controller, hass_admin_user, params, src_ieee, code
|
||||||
|
):
|
||||||
|
"""Test permit service with install code."""
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN, SERVICE_PERMIT, params, True, Context(user_id=hass_admin_user.id)
|
||||||
|
)
|
||||||
|
assert app_controller.permit.await_count == 0
|
||||||
|
assert app_controller.permit_with_key.call_count == 1
|
||||||
|
assert app_controller.permit_with_key.await_args[1]["time_s"] == 60
|
||||||
|
assert app_controller.permit_with_key.await_args[1]["node"] == src_ieee
|
||||||
|
assert app_controller.permit_with_key.await_args[1]["code"] == code
|
||||||
|
|
||||||
|
|
||||||
|
IC_FAIL_PARAMS = (
|
||||||
|
{
|
||||||
|
# wrong install code
|
||||||
|
ATTR_SOURCE_IEEE: IEEE_SWITCH_DEVICE,
|
||||||
|
ATTR_INSTALL_CODE: "5279-7BF4-A508-4DAA-8E17-12B6-1741-CA02-4052",
|
||||||
|
},
|
||||||
|
# incorrect service params
|
||||||
|
{ATTR_INSTALL_CODE: "5279-7BF4-A508-4DAA-8E17-12B6-1741-CA02-4051"},
|
||||||
|
{ATTR_SOURCE_IEEE: IEEE_SWITCH_DEVICE},
|
||||||
|
{
|
||||||
|
# incorrect service params
|
||||||
|
ATTR_INSTALL_CODE: "5279-7BF4-A508-4DAA-8E17-12B6-1741-CA02-4051",
|
||||||
|
ATTR_QR_CODE: "Z:000D6FFFFED4163B$I:52797BF4A5084DAA8E1712B61741CA024051",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
# incorrect service params
|
||||||
|
ATTR_SOURCE_IEEE: IEEE_SWITCH_DEVICE,
|
||||||
|
ATTR_QR_CODE: "Z:000D6FFFFED4163B$I:52797BF4A5084DAA8E1712B61741CA024051",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
# good regex match, but bad code
|
||||||
|
ATTR_QR_CODE: "Z:000D6FFFFED4163B$I:52797BF4A5084DAA8E1712B61741CA024052"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
# good aqara regex match, but bad code
|
||||||
|
ATTR_QR_CODE: (
|
||||||
|
"G$M:751$S:357S00001579$D:000000000F350FFD%Z$A:04CF8CDF"
|
||||||
|
"3C3C3C3C$I:52797BF4A5084DAA8E1712B61741CA024052"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
# good consciot regex match, but bad code
|
||||||
|
{ATTR_QR_CODE: "000D6FFFFED4163B|52797BF4A5084DAA8E1712B61741CA024052"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("params", IC_FAIL_PARAMS)
|
||||||
|
async def test_permit_with_install_code_fail(
|
||||||
|
hass, app_controller, hass_admin_user, params
|
||||||
|
):
|
||||||
|
"""Test permit service with install code."""
|
||||||
|
|
||||||
|
with pytest.raises(vol.Invalid):
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN, SERVICE_PERMIT, params, True, Context(user_id=hass_admin_user.id)
|
||||||
|
)
|
||||||
|
assert app_controller.permit.await_count == 0
|
||||||
|
assert app_controller.permit_with_key.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
IC_QR_CODE_TEST_PARAMS = (
|
||||||
|
(
|
||||||
|
{ATTR_QR_CODE: "000D6FFFFED4163B|52797BF4A5084DAA8E1712B61741CA024051"},
|
||||||
|
zigpy.types.EUI64.convert("00:0D:6F:FF:FE:D4:16:3B"),
|
||||||
|
unhexlify("52797BF4A5084DAA8E1712B61741CA024051"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{ATTR_QR_CODE: "Z:000D6FFFFED4163B$I:52797BF4A5084DAA8E1712B61741CA024051"},
|
||||||
|
zigpy.types.EUI64.convert("00:0D:6F:FF:FE:D4:16:3B"),
|
||||||
|
unhexlify("52797BF4A5084DAA8E1712B61741CA024051"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
ATTR_QR_CODE: (
|
||||||
|
"G$M:751$S:357S00001579$D:000000000F350FFD%Z$A:04CF8CDF"
|
||||||
|
"3C3C3C3C$I:52797BF4A5084DAA8E1712B61741CA024051"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
zigpy.types.EUI64.convert("04:CF:8C:DF:3C:3C:3C:3C"),
|
||||||
|
unhexlify("52797BF4A5084DAA8E1712B61741CA024051"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("params, src_ieee, code", IC_QR_CODE_TEST_PARAMS)
|
||||||
|
async def test_permit_with_qr_code(
|
||||||
|
hass, app_controller, hass_admin_user, params, src_ieee, code
|
||||||
|
):
|
||||||
|
"""Test permit service with install code from qr code."""
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN, SERVICE_PERMIT, params, True, Context(user_id=hass_admin_user.id)
|
||||||
|
)
|
||||||
|
assert app_controller.permit.await_count == 0
|
||||||
|
assert app_controller.permit_with_key.call_count == 1
|
||||||
|
assert app_controller.permit_with_key.await_args[1]["time_s"] == 60
|
||||||
|
assert app_controller.permit_with_key.await_args[1]["node"] == src_ieee
|
||||||
|
assert app_controller.permit_with_key.await_args[1]["code"] == code
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("params, src_ieee, code", IC_QR_CODE_TEST_PARAMS)
|
||||||
|
async def test_ws_permit_with_qr_code(
|
||||||
|
app_controller, zha_client, params, src_ieee, code
|
||||||
|
):
|
||||||
|
"""Test permit service with install code from qr code."""
|
||||||
|
|
||||||
|
await zha_client.send_json(
|
||||||
|
{ID: 14, TYPE: f"{DOMAIN}/devices/{SERVICE_PERMIT}", **params}
|
||||||
|
)
|
||||||
|
|
||||||
|
msg = await zha_client.receive_json()
|
||||||
|
assert msg["id"] == 14
|
||||||
|
assert msg["type"] == const.TYPE_RESULT
|
||||||
|
assert msg["success"]
|
||||||
|
|
||||||
|
assert app_controller.permit.await_count == 0
|
||||||
|
assert app_controller.permit_with_key.call_count == 1
|
||||||
|
assert app_controller.permit_with_key.await_args[1]["time_s"] == 60
|
||||||
|
assert app_controller.permit_with_key.await_args[1]["node"] == src_ieee
|
||||||
|
assert app_controller.permit_with_key.await_args[1]["code"] == code
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("params", IC_FAIL_PARAMS)
|
||||||
|
async def test_ws_permit_with_install_code_fail(app_controller, zha_client, params):
|
||||||
|
"""Test permit ws service with install code."""
|
||||||
|
|
||||||
|
await zha_client.send_json(
|
||||||
|
{ID: 14, TYPE: f"{DOMAIN}/devices/{SERVICE_PERMIT}", **params}
|
||||||
|
)
|
||||||
|
|
||||||
|
msg = await zha_client.receive_json()
|
||||||
|
assert msg["id"] == 14
|
||||||
|
assert msg["type"] == const.TYPE_RESULT
|
||||||
|
assert msg["success"] is False
|
||||||
|
|
||||||
|
assert app_controller.permit.await_count == 0
|
||||||
|
assert app_controller.permit_with_key.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"params, duration, node",
|
||||||
|
(
|
||||||
|
({}, 60, None),
|
||||||
|
({ATTR_DURATION: 30}, 30, None),
|
||||||
|
(
|
||||||
|
{ATTR_DURATION: 33, ATTR_IEEE: "aa:bb:cc:dd:aa:bb:cc:dd"},
|
||||||
|
33,
|
||||||
|
zigpy.types.EUI64.convert("aa:bb:cc:dd:aa:bb:cc:dd"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{ATTR_IEEE: "aa:bb:cc:dd:aa:bb:cc:d1"},
|
||||||
|
60,
|
||||||
|
zigpy.types.EUI64.convert("aa:bb:cc:dd:aa:bb:cc:d1"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def test_ws_permit_ha12(app_controller, zha_client, params, duration, node):
|
||||||
|
"""Test permit ws service."""
|
||||||
|
|
||||||
|
await zha_client.send_json(
|
||||||
|
{ID: 14, TYPE: f"{DOMAIN}/devices/{SERVICE_PERMIT}", **params}
|
||||||
|
)
|
||||||
|
|
||||||
|
msg = await zha_client.receive_json()
|
||||||
|
assert msg["id"] == 14
|
||||||
|
assert msg["type"] == const.TYPE_RESULT
|
||||||
|
assert msg["success"]
|
||||||
|
|
||||||
|
assert app_controller.permit.await_count == 1
|
||||||
|
assert app_controller.permit.await_args[1]["time_s"] == duration
|
||||||
|
assert app_controller.permit.await_args[1]["node"] == node
|
||||||
|
assert app_controller.permit_with_key.call_count == 0
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue