Add support for zwave_js device actions (#53038)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
552485bb05
commit
e690d4b006
11 changed files with 889 additions and 60 deletions
|
@ -47,13 +47,13 @@ from homeassistant.helpers.device_registry import DeviceEntry
|
|||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from .const import (
|
||||
BITMASK_SCHEMA,
|
||||
CONF_DATA_COLLECTION_OPTED_IN,
|
||||
DATA_CLIENT,
|
||||
DOMAIN,
|
||||
EVENT_DEVICE_ADDED_TO_REGISTRY,
|
||||
)
|
||||
from .helpers import async_enable_statistics, update_data_collection_preference
|
||||
from .services import BITMASK_SCHEMA
|
||||
|
||||
DATA_UNSUBSCRIBE = "unsubs"
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
"""Constants for the Z-Wave JS integration."""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
CONF_ADDON_DEVICE = "device"
|
||||
CONF_ADDON_EMULATE_HARDWARE = "emulate_hardware"
|
||||
CONF_ADDON_LOG_LEVEL = "log_level"
|
||||
|
@ -56,6 +60,8 @@ ATTR_CURRENT_VALUE_RAW = "current_value_raw"
|
|||
ATTR_DESCRIPTION = "description"
|
||||
|
||||
# service constants
|
||||
SERVICE_SET_LOCK_USERCODE = "set_lock_usercode"
|
||||
SERVICE_CLEAR_LOCK_USERCODE = "clear_lock_usercode"
|
||||
SERVICE_SET_VALUE = "set_value"
|
||||
SERVICE_RESET_METER = "reset_meter"
|
||||
SERVICE_MULTICAST_SET_VALUE = "multicast_set_value"
|
||||
|
@ -98,3 +104,25 @@ ENTITY_DESC_KEY_TARGET_TEMPERATURE = "target_temperature"
|
|||
ENTITY_DESC_KEY_TIMESTAMP = "timestamp"
|
||||
ENTITY_DESC_KEY_MEASUREMENT = "measurement"
|
||||
ENTITY_DESC_KEY_TOTAL_INCREASING = "total_increasing"
|
||||
|
||||
# Schema Constants
|
||||
|
||||
# Validates that a bitmask is provided in hex form and converts it to decimal
|
||||
# int equivalent since that's what the library uses
|
||||
BITMASK_SCHEMA = vol.All(
|
||||
cv.string,
|
||||
vol.Lower,
|
||||
vol.Match(
|
||||
r"^(0x)?[0-9a-f]+$",
|
||||
msg="Must provide an integer (e.g. 255) or a bitmask in hex form (e.g. 0xff)",
|
||||
),
|
||||
lambda value: int(value, 16),
|
||||
)
|
||||
|
||||
VALUE_SCHEMA = vol.Any(
|
||||
bool,
|
||||
vol.Coerce(int),
|
||||
vol.Coerce(float),
|
||||
BITMASK_SCHEMA,
|
||||
cv.string,
|
||||
)
|
||||
|
|
309
homeassistant/components/zwave_js/device_action.py
Normal file
309
homeassistant/components/zwave_js/device_action.py
Normal file
|
@ -0,0 +1,309 @@
|
|||
"""Provides device actions for Z-Wave JS."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
from zwave_js_server.const import CommandClass
|
||||
from zwave_js_server.const.command_class.lock import ATTR_CODE_SLOT, ATTR_USERCODE
|
||||
from zwave_js_server.const.command_class.meter import CC_SPECIFIC_METER_TYPE
|
||||
from zwave_js_server.model.value import get_value_id
|
||||
from zwave_js_server.util.command_class.meter import get_meter_type
|
||||
|
||||
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, CONF_TYPE
|
||||
from homeassistant.core import Context, HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
ATTR_COMMAND_CLASS,
|
||||
ATTR_CONFIG_PARAMETER,
|
||||
ATTR_CONFIG_PARAMETER_BITMASK,
|
||||
ATTR_ENDPOINT,
|
||||
ATTR_METER_TYPE,
|
||||
ATTR_PROPERTY,
|
||||
ATTR_PROPERTY_KEY,
|
||||
ATTR_REFRESH_ALL_VALUES,
|
||||
ATTR_VALUE,
|
||||
ATTR_WAIT_FOR_RESULT,
|
||||
DOMAIN,
|
||||
SERVICE_CLEAR_LOCK_USERCODE,
|
||||
SERVICE_PING,
|
||||
SERVICE_REFRESH_VALUE,
|
||||
SERVICE_RESET_METER,
|
||||
SERVICE_SET_CONFIG_PARAMETER,
|
||||
SERVICE_SET_LOCK_USERCODE,
|
||||
SERVICE_SET_VALUE,
|
||||
VALUE_SCHEMA,
|
||||
)
|
||||
from .device_automation_helpers import (
|
||||
CONF_SUBTYPE,
|
||||
VALUE_ID_REGEX,
|
||||
get_config_parameter_value_schema,
|
||||
)
|
||||
from .helpers import async_get_node_from_device_id
|
||||
|
||||
ACTION_TYPES = {
|
||||
SERVICE_CLEAR_LOCK_USERCODE,
|
||||
SERVICE_PING,
|
||||
SERVICE_REFRESH_VALUE,
|
||||
SERVICE_RESET_METER,
|
||||
SERVICE_SET_CONFIG_PARAMETER,
|
||||
SERVICE_SET_LOCK_USERCODE,
|
||||
SERVICE_SET_VALUE,
|
||||
}
|
||||
|
||||
CLEAR_LOCK_USERCODE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_TYPE): SERVICE_CLEAR_LOCK_USERCODE,
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_domain(LOCK_DOMAIN),
|
||||
vol.Required(ATTR_CODE_SLOT): vol.Coerce(int),
|
||||
}
|
||||
)
|
||||
|
||||
PING_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_TYPE): SERVICE_PING,
|
||||
}
|
||||
)
|
||||
|
||||
REFRESH_VALUE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_TYPE): SERVICE_REFRESH_VALUE,
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Optional(ATTR_REFRESH_ALL_VALUES, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
RESET_METER_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_TYPE): SERVICE_RESET_METER,
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_domain(SENSOR_DOMAIN),
|
||||
vol.Optional(ATTR_METER_TYPE): vol.Coerce(int),
|
||||
vol.Optional(ATTR_VALUE): vol.Coerce(int),
|
||||
}
|
||||
)
|
||||
|
||||
SET_CONFIG_PARAMETER_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_TYPE): SERVICE_SET_CONFIG_PARAMETER,
|
||||
vol.Required(ATTR_CONFIG_PARAMETER): vol.Any(int, str),
|
||||
vol.Required(ATTR_CONFIG_PARAMETER_BITMASK): vol.Any(None, int, str),
|
||||
vol.Required(ATTR_VALUE): vol.Coerce(int),
|
||||
vol.Required(CONF_SUBTYPE): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
SET_LOCK_USERCODE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_TYPE): SERVICE_SET_LOCK_USERCODE,
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_domain(LOCK_DOMAIN),
|
||||
vol.Required(ATTR_CODE_SLOT): vol.Coerce(int),
|
||||
vol.Required(ATTR_USERCODE): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
SET_VALUE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_TYPE): SERVICE_SET_VALUE,
|
||||
vol.Required(ATTR_COMMAND_CLASS): vol.In([cc.value for cc in CommandClass]),
|
||||
vol.Required(ATTR_PROPERTY): vol.Any(int, str),
|
||||
vol.Optional(ATTR_PROPERTY_KEY): vol.Any(vol.Coerce(int), cv.string),
|
||||
vol.Optional(ATTR_ENDPOINT): vol.Coerce(int),
|
||||
vol.Required(ATTR_VALUE): VALUE_SCHEMA,
|
||||
vol.Optional(ATTR_WAIT_FOR_RESULT, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
ACTION_SCHEMA = vol.Any(
|
||||
CLEAR_LOCK_USERCODE_SCHEMA,
|
||||
PING_SCHEMA,
|
||||
REFRESH_VALUE_SCHEMA,
|
||||
RESET_METER_SCHEMA,
|
||||
SET_CONFIG_PARAMETER_SCHEMA,
|
||||
SET_LOCK_USERCODE_SCHEMA,
|
||||
SET_VALUE_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
|
||||
"""List device actions for Z-Wave JS devices."""
|
||||
registry = entity_registry.async_get(hass)
|
||||
actions = []
|
||||
|
||||
node = async_get_node_from_device_id(hass, device_id)
|
||||
|
||||
base_action = {
|
||||
CONF_DEVICE_ID: device_id,
|
||||
CONF_DOMAIN: DOMAIN,
|
||||
}
|
||||
|
||||
actions.extend(
|
||||
[
|
||||
{**base_action, CONF_TYPE: SERVICE_SET_VALUE},
|
||||
{**base_action, CONF_TYPE: SERVICE_PING},
|
||||
]
|
||||
)
|
||||
actions.extend(
|
||||
[
|
||||
{
|
||||
**base_action,
|
||||
CONF_TYPE: SERVICE_SET_CONFIG_PARAMETER,
|
||||
ATTR_CONFIG_PARAMETER: config_value.property_,
|
||||
ATTR_CONFIG_PARAMETER_BITMASK: config_value.property_key,
|
||||
CONF_SUBTYPE: f"{config_value.value_id} ({config_value.property_name})",
|
||||
}
|
||||
for config_value in node.get_configuration_values().values()
|
||||
]
|
||||
)
|
||||
|
||||
meter_endpoints: dict[int, dict[str, Any]] = defaultdict(dict)
|
||||
|
||||
for entry in entity_registry.async_entries_for_device(registry, device_id):
|
||||
entity_action = {**base_action, CONF_ENTITY_ID: entry.entity_id}
|
||||
actions.append({**entity_action, CONF_TYPE: SERVICE_REFRESH_VALUE})
|
||||
if entry.domain == LOCK_DOMAIN:
|
||||
actions.extend(
|
||||
[
|
||||
{**entity_action, CONF_TYPE: SERVICE_SET_LOCK_USERCODE},
|
||||
{**entity_action, CONF_TYPE: SERVICE_CLEAR_LOCK_USERCODE},
|
||||
]
|
||||
)
|
||||
|
||||
if entry.domain == SENSOR_DOMAIN:
|
||||
value_id = entry.unique_id.split(".")[1]
|
||||
# If this unique ID doesn't have a value ID, we know it is the node status
|
||||
# sensor which doesn't have any relevant actions
|
||||
if re.match(VALUE_ID_REGEX, value_id):
|
||||
value = node.values[value_id]
|
||||
else:
|
||||
continue
|
||||
# If the value has the meterType CC specific value, we can add a reset_meter
|
||||
# action for it
|
||||
if CC_SPECIFIC_METER_TYPE in value.metadata.cc_specific:
|
||||
meter_endpoints[value.endpoint].setdefault(
|
||||
CONF_ENTITY_ID, entry.entity_id
|
||||
)
|
||||
meter_endpoints[value.endpoint].setdefault(ATTR_METER_TYPE, set()).add(
|
||||
get_meter_type(value)
|
||||
)
|
||||
|
||||
if not meter_endpoints:
|
||||
return actions
|
||||
|
||||
for endpoint, endpoint_data in meter_endpoints.items():
|
||||
base_action[CONF_ENTITY_ID] = endpoint_data[CONF_ENTITY_ID]
|
||||
actions.append(
|
||||
{
|
||||
**base_action,
|
||||
CONF_TYPE: SERVICE_RESET_METER,
|
||||
CONF_SUBTYPE: f"Endpoint {endpoint} (All)",
|
||||
}
|
||||
)
|
||||
for meter_type in endpoint_data[ATTR_METER_TYPE]:
|
||||
actions.append(
|
||||
{
|
||||
**base_action,
|
||||
CONF_TYPE: SERVICE_RESET_METER,
|
||||
ATTR_METER_TYPE: meter_type,
|
||||
CONF_SUBTYPE: f"Endpoint {endpoint} ({meter_type.name})",
|
||||
}
|
||||
)
|
||||
|
||||
return actions
|
||||
|
||||
|
||||
async def async_call_action_from_config(
|
||||
hass: HomeAssistant, config: dict, variables: dict, context: Context | None
|
||||
) -> None:
|
||||
"""Execute a device action."""
|
||||
action_type = service = config.pop(CONF_TYPE)
|
||||
if action_type not in ACTION_TYPES:
|
||||
raise HomeAssistantError(f"Unhandled action type {action_type}")
|
||||
|
||||
service_data = {k: v for k, v in config.items() if v not in (None, "")}
|
||||
await hass.services.async_call(
|
||||
DOMAIN, service, service_data, blocking=True, context=context
|
||||
)
|
||||
|
||||
|
||||
async def async_get_action_capabilities(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
) -> dict[str, vol.Schema]:
|
||||
"""List action capabilities."""
|
||||
action_type = config[CONF_TYPE]
|
||||
node = async_get_node_from_device_id(hass, config[CONF_DEVICE_ID])
|
||||
|
||||
# Add additional fields to the automation action UI
|
||||
if action_type == SERVICE_CLEAR_LOCK_USERCODE:
|
||||
return {
|
||||
"extra_fields": vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CODE_SLOT): cv.string,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if action_type == SERVICE_SET_LOCK_USERCODE:
|
||||
return {
|
||||
"extra_fields": vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CODE_SLOT): cv.string,
|
||||
vol.Required(ATTR_USERCODE): cv.string,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if action_type == SERVICE_RESET_METER:
|
||||
return {
|
||||
"extra_fields": vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_VALUE): cv.string,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if action_type == SERVICE_REFRESH_VALUE:
|
||||
return {
|
||||
"extra_fields": vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_REFRESH_ALL_VALUES): cv.boolean,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if action_type == SERVICE_SET_VALUE:
|
||||
return {
|
||||
"extra_fields": vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_COMMAND_CLASS): vol.In(
|
||||
{cc.value: cc.name for cc in CommandClass}
|
||||
),
|
||||
vol.Required(ATTR_PROPERTY): cv.string,
|
||||
vol.Optional(ATTR_PROPERTY_KEY): cv.string,
|
||||
vol.Optional(ATTR_ENDPOINT): cv.string,
|
||||
vol.Required(ATTR_VALUE): cv.string,
|
||||
vol.Optional(ATTR_WAIT_FOR_RESULT): cv.boolean,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if action_type == SERVICE_SET_CONFIG_PARAMETER:
|
||||
value_id = get_value_id(
|
||||
node,
|
||||
CommandClass.CONFIGURATION,
|
||||
config[ATTR_CONFIG_PARAMETER],
|
||||
property_key=config[ATTR_CONFIG_PARAMETER_BITMASK],
|
||||
)
|
||||
value_schema = get_config_parameter_value_schema(node, value_id)
|
||||
if value_schema is None:
|
||||
return {}
|
||||
return {"extra_fields": vol.Schema({vol.Required(ATTR_VALUE): value_schema})}
|
||||
|
||||
return {}
|
|
@ -0,0 +1,34 @@
|
|||
"""Provides helpers for Z-Wave JS device automations."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import cast
|
||||
|
||||
import voluptuous as vol
|
||||
from zwave_js_server.const import ConfigurationValueType
|
||||
from zwave_js_server.model.node import Node
|
||||
from zwave_js_server.model.value import ConfigurationValue
|
||||
|
||||
NODE_STATUSES = ["asleep", "awake", "dead", "alive"]
|
||||
|
||||
CONF_SUBTYPE = "subtype"
|
||||
CONF_VALUE_ID = "value_id"
|
||||
|
||||
VALUE_ID_REGEX = r"([0-9]+-[0-9]+-[0-9]+-).+"
|
||||
|
||||
|
||||
def get_config_parameter_value_schema(node: Node, value_id: str) -> vol.Schema | None:
|
||||
"""Get the extra fields schema for a config parameter value."""
|
||||
config_value = cast(ConfigurationValue, node.values[value_id])
|
||||
min_ = config_value.metadata.min
|
||||
max_ = config_value.metadata.max
|
||||
|
||||
if config_value.configuration_value_type in (
|
||||
ConfigurationValueType.RANGE,
|
||||
ConfigurationValueType.MANUAL_ENTRY,
|
||||
):
|
||||
return vol.All(vol.Coerce(int), vol.Range(min=min_, max=max_))
|
||||
|
||||
if config_value.configuration_value_type == ConfigurationValueType.ENUMERATED:
|
||||
return vol.In({int(k): v for k, v in config_value.metadata.states.items()})
|
||||
|
||||
return None
|
|
@ -14,7 +14,6 @@ from homeassistant.const import CONF_CONDITION, CONF_DEVICE_ID, CONF_DOMAIN, CON
|
|||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import condition, config_validation as cv
|
||||
from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA
|
||||
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
||||
|
||||
from . import DOMAIN
|
||||
|
@ -24,34 +23,37 @@ from .const import (
|
|||
ATTR_PROPERTY,
|
||||
ATTR_PROPERTY_KEY,
|
||||
ATTR_VALUE,
|
||||
VALUE_SCHEMA,
|
||||
)
|
||||
from .device_automation_helpers import (
|
||||
CONF_SUBTYPE,
|
||||
CONF_VALUE_ID,
|
||||
NODE_STATUSES,
|
||||
get_config_parameter_value_schema,
|
||||
)
|
||||
from .helpers import (
|
||||
async_get_node_from_device_id,
|
||||
async_is_device_config_entry_not_loaded,
|
||||
check_type_schema_map,
|
||||
get_value_state_schema,
|
||||
get_zwave_value_from_config,
|
||||
remove_keys_with_empty_values,
|
||||
)
|
||||
|
||||
CONF_SUBTYPE = "subtype"
|
||||
CONF_VALUE_ID = "value_id"
|
||||
CONF_STATUS = "status"
|
||||
|
||||
NODE_STATUS_TYPE = "node_status"
|
||||
NODE_STATUS_TYPES = ["asleep", "awake", "dead", "alive"]
|
||||
CONFIG_PARAMETER_TYPE = "config_parameter"
|
||||
VALUE_TYPE = "value"
|
||||
CONDITION_TYPES = {NODE_STATUS_TYPE, CONFIG_PARAMETER_TYPE, VALUE_TYPE}
|
||||
|
||||
NODE_STATUS_CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend(
|
||||
NODE_STATUS_CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_TYPE): NODE_STATUS_TYPE,
|
||||
vol.Required(CONF_STATUS): vol.In(NODE_STATUS_TYPES),
|
||||
vol.Required(CONF_STATUS): vol.In(NODE_STATUSES),
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_PARAMETER_CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend(
|
||||
CONFIG_PARAMETER_CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_TYPE): CONFIG_PARAMETER_TYPE,
|
||||
vol.Required(CONF_VALUE_ID): cv.string,
|
||||
|
@ -60,20 +62,14 @@ CONFIG_PARAMETER_CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend(
|
|||
}
|
||||
)
|
||||
|
||||
VALUE_CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend(
|
||||
VALUE_CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_TYPE): VALUE_TYPE,
|
||||
vol.Required(ATTR_COMMAND_CLASS): vol.In([cc.value for cc in CommandClass]),
|
||||
vol.Required(ATTR_PROPERTY): vol.Any(vol.Coerce(int), cv.string),
|
||||
vol.Optional(ATTR_PROPERTY_KEY): vol.Any(vol.Coerce(int), cv.string),
|
||||
vol.Optional(ATTR_ENDPOINT): vol.Coerce(int),
|
||||
vol.Required(ATTR_VALUE): vol.Any(
|
||||
bool,
|
||||
vol.Coerce(int),
|
||||
vol.Coerce(float),
|
||||
cv.boolean,
|
||||
cv.string,
|
||||
),
|
||||
vol.Required(ATTR_VALUE): VALUE_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -204,10 +200,9 @@ async def async_get_condition_capabilities(
|
|||
# Add additional fields to the automation trigger UI
|
||||
if config[CONF_TYPE] == CONFIG_PARAMETER_TYPE:
|
||||
value_id = config[CONF_VALUE_ID]
|
||||
value_schema = get_value_state_schema(node.values[value_id])
|
||||
if not value_schema:
|
||||
value_schema = get_config_parameter_value_schema(node, value_id)
|
||||
if value_schema is None:
|
||||
return {}
|
||||
|
||||
return {"extra_fields": vol.Schema({vol.Required(ATTR_VALUE): value_schema})}
|
||||
|
||||
if config[CONF_TYPE] == VALUE_TYPE:
|
||||
|
@ -234,7 +229,7 @@ async def async_get_condition_capabilities(
|
|||
if config[CONF_TYPE] == NODE_STATUS_TYPE:
|
||||
return {
|
||||
"extra_fields": vol.Schema(
|
||||
{vol.Required(CONF_STATUS): vol.In(NODE_STATUS_TYPES)}
|
||||
{vol.Required(CONF_STATUS): vol.In(NODE_STATUSES)}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ from .const import (
|
|||
ZWAVE_JS_NOTIFICATION_EVENT,
|
||||
ZWAVE_JS_VALUE_NOTIFICATION_EVENT,
|
||||
)
|
||||
from .device_automation_helpers import CONF_SUBTYPE, NODE_STATUSES
|
||||
from .helpers import (
|
||||
async_get_node_from_device_id,
|
||||
async_get_node_status_sensor_entity_id,
|
||||
|
@ -65,8 +66,6 @@ from .triggers.value_updated import (
|
|||
PLATFORM_TYPE as VALUE_UPDATED_PLATFORM_TYPE,
|
||||
)
|
||||
|
||||
CONF_SUBTYPE = "subtype"
|
||||
|
||||
# Trigger types
|
||||
ENTRY_CONTROL_NOTIFICATION = "event.notification.entry_control"
|
||||
NOTIFICATION_NOTIFICATION = "event.notification.notification"
|
||||
|
@ -153,8 +152,6 @@ BASE_STATE_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
|
|||
}
|
||||
)
|
||||
|
||||
NODE_STATUSES = ["asleep", "awake", "dead", "alive"]
|
||||
|
||||
NODE_STATUS_SCHEMA = BASE_STATE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_TYPE): NODE_STATUS,
|
||||
|
|
|
@ -25,7 +25,12 @@ from homeassistant.helpers import config_validation as cv, entity_platform
|
|||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DATA_CLIENT, DOMAIN
|
||||
from .const import (
|
||||
DATA_CLIENT,
|
||||
DOMAIN,
|
||||
SERVICE_CLEAR_LOCK_USERCODE,
|
||||
SERVICE_SET_LOCK_USERCODE,
|
||||
)
|
||||
from .discovery import ZwaveDiscoveryInfo
|
||||
from .entity import ZWaveBaseEntity
|
||||
|
||||
|
@ -42,9 +47,6 @@ STATE_TO_ZWAVE_MAP: dict[int, dict[str, int | bool]] = {
|
|||
},
|
||||
}
|
||||
|
||||
SERVICE_SET_LOCK_USERCODE = "set_lock_usercode"
|
||||
SERVICE_CLEAR_LOCK_USERCODE = "clear_lock_usercode"
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
|
|
|
@ -59,27 +59,6 @@ def broadcast_command(val: dict[str, Any]) -> dict[str, Any]:
|
|||
)
|
||||
|
||||
|
||||
# Validates that a bitmask is provided in hex form and converts it to decimal
|
||||
# int equivalent since that's what the library uses
|
||||
BITMASK_SCHEMA = vol.All(
|
||||
cv.string,
|
||||
vol.Lower,
|
||||
vol.Match(
|
||||
r"^(0x)?[0-9a-f]+$",
|
||||
msg="Must provide an integer (e.g. 255) or a bitmask in hex form (e.g. 0xff)",
|
||||
),
|
||||
lambda value: int(value, 16),
|
||||
)
|
||||
|
||||
VALUE_SCHEMA = vol.Any(
|
||||
bool,
|
||||
vol.Coerce(int),
|
||||
vol.Coerce(float),
|
||||
BITMASK_SCHEMA,
|
||||
cv.string,
|
||||
)
|
||||
|
||||
|
||||
class ZWaveServices:
|
||||
"""Class that holds our services (Zwave Commands) that should be published to hass."""
|
||||
|
||||
|
@ -198,10 +177,10 @@ class ZWaveServices:
|
|||
vol.Coerce(int), cv.string
|
||||
),
|
||||
vol.Optional(const.ATTR_CONFIG_PARAMETER_BITMASK): vol.Any(
|
||||
vol.Coerce(int), BITMASK_SCHEMA
|
||||
vol.Coerce(int), const.BITMASK_SCHEMA
|
||||
),
|
||||
vol.Required(const.ATTR_CONFIG_VALUE): vol.Any(
|
||||
vol.Coerce(int), BITMASK_SCHEMA, cv.string
|
||||
vol.Coerce(int), const.BITMASK_SCHEMA, cv.string
|
||||
),
|
||||
},
|
||||
cv.has_at_least_one_key(
|
||||
|
@ -232,8 +211,10 @@ class ZWaveServices:
|
|||
vol.Coerce(int),
|
||||
{
|
||||
vol.Any(
|
||||
vol.Coerce(int), BITMASK_SCHEMA, cv.string
|
||||
): vol.Any(vol.Coerce(int), BITMASK_SCHEMA, cv.string)
|
||||
vol.Coerce(int), const.BITMASK_SCHEMA, cv.string
|
||||
): vol.Any(
|
||||
vol.Coerce(int), const.BITMASK_SCHEMA, cv.string
|
||||
)
|
||||
},
|
||||
),
|
||||
},
|
||||
|
@ -284,9 +265,11 @@ class ZWaveServices:
|
|||
vol.Coerce(int), str
|
||||
),
|
||||
vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int),
|
||||
vol.Required(const.ATTR_VALUE): VALUE_SCHEMA,
|
||||
vol.Required(const.ATTR_VALUE): const.VALUE_SCHEMA,
|
||||
vol.Optional(const.ATTR_WAIT_FOR_RESULT): cv.boolean,
|
||||
vol.Optional(const.ATTR_OPTIONS): {cv.string: VALUE_SCHEMA},
|
||||
vol.Optional(const.ATTR_OPTIONS): {
|
||||
cv.string: const.VALUE_SCHEMA
|
||||
},
|
||||
},
|
||||
cv.has_at_least_one_key(
|
||||
ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_AREA_ID
|
||||
|
@ -319,8 +302,10 @@ class ZWaveServices:
|
|||
vol.Coerce(int), str
|
||||
),
|
||||
vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int),
|
||||
vol.Required(const.ATTR_VALUE): VALUE_SCHEMA,
|
||||
vol.Optional(const.ATTR_OPTIONS): {cv.string: VALUE_SCHEMA},
|
||||
vol.Required(const.ATTR_VALUE): const.VALUE_SCHEMA,
|
||||
vol.Optional(const.ATTR_OPTIONS): {
|
||||
cv.string: const.VALUE_SCHEMA
|
||||
},
|
||||
},
|
||||
vol.Any(
|
||||
cv.has_at_least_one_key(
|
||||
|
|
|
@ -67,7 +67,9 @@
|
|||
"on_supervisor": {
|
||||
"title": "Select connection method",
|
||||
"description": "Do you want to use the Z-Wave JS Supervisor add-on?",
|
||||
"data": { "use_addon": "Use the Z-Wave JS Supervisor add-on" }
|
||||
"data": {
|
||||
"use_addon": "Use the Z-Wave JS Supervisor add-on"
|
||||
}
|
||||
},
|
||||
"install_addon": {
|
||||
"title": "The Z-Wave JS add-on installation has started"
|
||||
|
@ -81,7 +83,9 @@
|
|||
"emulate_hardware": "Emulate Hardware"
|
||||
}
|
||||
},
|
||||
"start_addon": { "title": "The Z-Wave JS add-on is starting." }
|
||||
"start_addon": {
|
||||
"title": "The Z-Wave JS add-on is starting."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_ws_url": "Invalid websocket URL",
|
||||
|
@ -118,6 +122,15 @@
|
|||
"node_status": "Node status",
|
||||
"config_parameter": "Config parameter {subtype} value",
|
||||
"value": "Current value of a Z-Wave Value"
|
||||
},
|
||||
"action_type": {
|
||||
"clear_lock_usercode": "Clear usercode on {entity_name}",
|
||||
"set_lock_usercode": "Set a usercode on {entity_name}",
|
||||
"set_config_parameter": "Set value of config parameter {subtype}",
|
||||
"set_value": "Set value of a Z-Wave Value",
|
||||
"refresh_value": "Refresh the value(s) for {entity_name}",
|
||||
"ping": "Ping device",
|
||||
"reset_meter": "Reset meters on {subtype}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,6 +58,15 @@
|
|||
}
|
||||
},
|
||||
"device_automation": {
|
||||
"action_type": {
|
||||
"clear_lock_usercode": "Clear usercode on {entity_name}",
|
||||
"ping": "Ping device",
|
||||
"refresh_value": "Refresh the value(s) for {entity_name}",
|
||||
"reset_meter": "Reset meters on {subtype}",
|
||||
"set_config_parameter": "Set value of config parameter {subtype}",
|
||||
"set_lock_usercode": "Set a usercode on {entity_name}",
|
||||
"set_value": "Set value of a Z-Wave Value"
|
||||
},
|
||||
"condition_type": {
|
||||
"config_parameter": "Config parameter {subtype} value",
|
||||
"node_status": "Node status",
|
||||
|
|
457
tests/components/zwave_js/test_device_action.py
Normal file
457
tests/components/zwave_js/test_device_action.py
Normal file
|
@ -0,0 +1,457 @@
|
|||
"""The tests for Z-Wave JS device actions."""
|
||||
import pytest
|
||||
import voluptuous_serialize
|
||||
from zwave_js_server.client import Client
|
||||
from zwave_js_server.const import CommandClass
|
||||
from zwave_js_server.model.node import Node
|
||||
|
||||
from homeassistant.components import automation
|
||||
from homeassistant.components.zwave_js import DOMAIN, device_action
|
||||
from homeassistant.components.zwave_js.helpers import get_device_id
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, device_registry
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import async_get_device_automations, async_mock_service
|
||||
|
||||
|
||||
async def test_get_actions(
|
||||
hass: HomeAssistant,
|
||||
client: Client,
|
||||
lock_schlage_be469: Node,
|
||||
integration: ConfigEntry,
|
||||
) -> None:
|
||||
"""Test we get the expected actions from a zwave_js node."""
|
||||
node = lock_schlage_be469
|
||||
dev_reg = device_registry.async_get(hass)
|
||||
device = dev_reg.async_get_device({get_device_id(client, node)})
|
||||
assert device
|
||||
expected_actions = [
|
||||
{
|
||||
"domain": DOMAIN,
|
||||
"type": "clear_lock_usercode",
|
||||
"device_id": device.id,
|
||||
"entity_id": "lock.touchscreen_deadbolt",
|
||||
},
|
||||
{
|
||||
"domain": DOMAIN,
|
||||
"type": "set_lock_usercode",
|
||||
"device_id": device.id,
|
||||
"entity_id": "lock.touchscreen_deadbolt",
|
||||
},
|
||||
{
|
||||
"domain": DOMAIN,
|
||||
"type": "refresh_value",
|
||||
"device_id": device.id,
|
||||
"entity_id": "lock.touchscreen_deadbolt",
|
||||
},
|
||||
{
|
||||
"domain": DOMAIN,
|
||||
"type": "set_value",
|
||||
"device_id": device.id,
|
||||
},
|
||||
{
|
||||
"domain": DOMAIN,
|
||||
"type": "ping",
|
||||
"device_id": device.id,
|
||||
},
|
||||
{
|
||||
"domain": DOMAIN,
|
||||
"type": "set_config_parameter",
|
||||
"device_id": device.id,
|
||||
"parameter": 3,
|
||||
"bitmask": None,
|
||||
"subtype": f"{node.node_id}-112-0-3 (Beeper)",
|
||||
},
|
||||
]
|
||||
actions = await async_get_device_automations(hass, "action", device.id)
|
||||
for action in expected_actions:
|
||||
assert action in actions
|
||||
|
||||
|
||||
async def test_get_actions_meter(
|
||||
hass: HomeAssistant,
|
||||
client: Client,
|
||||
aeon_smart_switch_6: Node,
|
||||
integration: ConfigEntry,
|
||||
) -> None:
|
||||
"""Test we get the expected meter actions from a zwave_js node."""
|
||||
node = aeon_smart_switch_6
|
||||
dev_reg = device_registry.async_get(hass)
|
||||
device = dev_reg.async_get_device({get_device_id(client, node)})
|
||||
assert device
|
||||
actions = await async_get_device_automations(hass, "action", device.id)
|
||||
filtered_actions = [action for action in actions if action["type"] == "reset_meter"]
|
||||
assert len(filtered_actions) > 0
|
||||
|
||||
|
||||
async def test_action(hass: HomeAssistant) -> None:
|
||||
"""Test for turn_on and turn_off actions."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: [
|
||||
{
|
||||
"trigger": {
|
||||
"platform": "event",
|
||||
"event_type": "test_event_clear_lock_usercode",
|
||||
},
|
||||
"action": {
|
||||
"domain": DOMAIN,
|
||||
"type": "clear_lock_usercode",
|
||||
"device_id": "fake",
|
||||
"entity_id": "lock.touchscreen_deadbolt",
|
||||
"code_slot": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
"trigger": {
|
||||
"platform": "event",
|
||||
"event_type": "test_event_set_lock_usercode",
|
||||
},
|
||||
"action": {
|
||||
"domain": DOMAIN,
|
||||
"type": "set_lock_usercode",
|
||||
"device_id": "fake",
|
||||
"entity_id": "lock.touchscreen_deadbolt",
|
||||
"code_slot": 1,
|
||||
"usercode": "1234",
|
||||
},
|
||||
},
|
||||
{
|
||||
"trigger": {
|
||||
"platform": "event",
|
||||
"event_type": "test_event_refresh_value",
|
||||
},
|
||||
"action": {
|
||||
"domain": DOMAIN,
|
||||
"type": "refresh_value",
|
||||
"device_id": "fake",
|
||||
"entity_id": "lock.touchscreen_deadbolt",
|
||||
},
|
||||
},
|
||||
{
|
||||
"trigger": {
|
||||
"platform": "event",
|
||||
"event_type": "test_event_ping",
|
||||
},
|
||||
"action": {
|
||||
"domain": DOMAIN,
|
||||
"type": "ping",
|
||||
"device_id": "fake",
|
||||
},
|
||||
},
|
||||
{
|
||||
"trigger": {
|
||||
"platform": "event",
|
||||
"event_type": "test_event_set_value",
|
||||
},
|
||||
"action": {
|
||||
"domain": DOMAIN,
|
||||
"type": "set_value",
|
||||
"device_id": "fake",
|
||||
"command_class": 112,
|
||||
"property": "test",
|
||||
"value": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
"trigger": {
|
||||
"platform": "event",
|
||||
"event_type": "test_event_set_config_parameter",
|
||||
},
|
||||
"action": {
|
||||
"domain": DOMAIN,
|
||||
"type": "set_config_parameter",
|
||||
"device_id": "fake",
|
||||
"parameter": 3,
|
||||
"bitmask": None,
|
||||
"subtype": "2-112-0-3 (Beeper)",
|
||||
"value": 255,
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
clear_lock_usercode = async_mock_service(hass, "zwave_js", "clear_lock_usercode")
|
||||
hass.bus.async_fire("test_event_clear_lock_usercode")
|
||||
await hass.async_block_till_done()
|
||||
assert len(clear_lock_usercode) == 1
|
||||
|
||||
set_lock_usercode = async_mock_service(hass, "zwave_js", "set_lock_usercode")
|
||||
hass.bus.async_fire("test_event_set_lock_usercode")
|
||||
await hass.async_block_till_done()
|
||||
assert len(set_lock_usercode) == 1
|
||||
|
||||
refresh_value = async_mock_service(hass, "zwave_js", "refresh_value")
|
||||
hass.bus.async_fire("test_event_refresh_value")
|
||||
await hass.async_block_till_done()
|
||||
assert len(refresh_value) == 1
|
||||
|
||||
ping = async_mock_service(hass, "zwave_js", "ping")
|
||||
hass.bus.async_fire("test_event_ping")
|
||||
await hass.async_block_till_done()
|
||||
assert len(ping) == 1
|
||||
|
||||
set_value = async_mock_service(hass, "zwave_js", "set_value")
|
||||
hass.bus.async_fire("test_event_set_value")
|
||||
await hass.async_block_till_done()
|
||||
assert len(set_value) == 1
|
||||
|
||||
set_config_parameter = async_mock_service(hass, "zwave_js", "set_config_parameter")
|
||||
hass.bus.async_fire("test_event_set_config_parameter")
|
||||
await hass.async_block_till_done()
|
||||
assert len(set_config_parameter) == 1
|
||||
|
||||
|
||||
async def test_get_action_capabilities(
|
||||
hass: HomeAssistant,
|
||||
client: Client,
|
||||
climate_radio_thermostat_ct100_plus: Node,
|
||||
integration: ConfigEntry,
|
||||
):
|
||||
"""Test we get the expected action capabilities."""
|
||||
node = climate_radio_thermostat_ct100_plus
|
||||
dev_reg = device_registry.async_get(hass)
|
||||
device = device_registry.async_entries_for_config_entry(
|
||||
dev_reg, integration.entry_id
|
||||
)[0]
|
||||
|
||||
# Test refresh_value
|
||||
capabilities = await device_action.async_get_action_capabilities(
|
||||
hass,
|
||||
{
|
||||
"platform": "device",
|
||||
"domain": DOMAIN,
|
||||
"device_id": device.id,
|
||||
"type": "refresh_value",
|
||||
},
|
||||
)
|
||||
assert capabilities and "extra_fields" in capabilities
|
||||
|
||||
assert voluptuous_serialize.convert(
|
||||
capabilities["extra_fields"], custom_serializer=cv.custom_serializer
|
||||
) == [{"type": "boolean", "name": "refresh_all_values", "optional": True}]
|
||||
|
||||
# Test ping
|
||||
capabilities = await device_action.async_get_action_capabilities(
|
||||
hass,
|
||||
{
|
||||
"platform": "device",
|
||||
"domain": DOMAIN,
|
||||
"device_id": device.id,
|
||||
"type": "ping",
|
||||
},
|
||||
)
|
||||
assert not capabilities
|
||||
|
||||
# Test set_value
|
||||
capabilities = await device_action.async_get_action_capabilities(
|
||||
hass,
|
||||
{
|
||||
"platform": "device",
|
||||
"domain": DOMAIN,
|
||||
"device_id": device.id,
|
||||
"type": "set_value",
|
||||
},
|
||||
)
|
||||
assert capabilities and "extra_fields" in capabilities
|
||||
|
||||
cc_options = [(cc.value, cc.name) for cc in CommandClass]
|
||||
|
||||
assert voluptuous_serialize.convert(
|
||||
capabilities["extra_fields"], custom_serializer=cv.custom_serializer
|
||||
) == [
|
||||
{
|
||||
"name": "command_class",
|
||||
"required": True,
|
||||
"options": cc_options,
|
||||
"type": "select",
|
||||
},
|
||||
{"name": "property", "required": True, "type": "string"},
|
||||
{"name": "property_key", "optional": True, "type": "string"},
|
||||
{"name": "endpoint", "optional": True, "type": "string"},
|
||||
{"name": "value", "required": True, "type": "string"},
|
||||
{"type": "boolean", "name": "wait_for_result", "optional": True},
|
||||
]
|
||||
|
||||
# Test enumerated type param
|
||||
capabilities = await device_action.async_get_action_capabilities(
|
||||
hass,
|
||||
{
|
||||
"platform": "device",
|
||||
"domain": DOMAIN,
|
||||
"device_id": device.id,
|
||||
"type": "set_config_parameter",
|
||||
"parameter": 1,
|
||||
"bitmask": None,
|
||||
"subtype": f"{node.node_id}-112-0-1 (Temperature Reporting Threshold)",
|
||||
},
|
||||
)
|
||||
assert capabilities and "extra_fields" in capabilities
|
||||
|
||||
assert voluptuous_serialize.convert(
|
||||
capabilities["extra_fields"], custom_serializer=cv.custom_serializer
|
||||
) == [
|
||||
{
|
||||
"name": "value",
|
||||
"required": True,
|
||||
"options": [
|
||||
(0, "Disabled"),
|
||||
(1, "0.5° F"),
|
||||
(2, "1.0° F"),
|
||||
(3, "1.5° F"),
|
||||
(4, "2.0° F"),
|
||||
],
|
||||
"type": "select",
|
||||
}
|
||||
]
|
||||
|
||||
# Test range type param
|
||||
capabilities = await device_action.async_get_action_capabilities(
|
||||
hass,
|
||||
{
|
||||
"platform": "device",
|
||||
"domain": DOMAIN,
|
||||
"device_id": device.id,
|
||||
"type": "set_config_parameter",
|
||||
"parameter": 10,
|
||||
"bitmask": None,
|
||||
"subtype": f"{node.node_id}-112-0-10 (Temperature Reporting Filter)",
|
||||
},
|
||||
)
|
||||
assert capabilities and "extra_fields" in capabilities
|
||||
|
||||
assert voluptuous_serialize.convert(
|
||||
capabilities["extra_fields"], custom_serializer=cv.custom_serializer
|
||||
) == [
|
||||
{
|
||||
"name": "value",
|
||||
"required": True,
|
||||
"type": "integer",
|
||||
"valueMin": 0,
|
||||
"valueMax": 124,
|
||||
}
|
||||
]
|
||||
|
||||
# Test undefined type param
|
||||
capabilities = await device_action.async_get_action_capabilities(
|
||||
hass,
|
||||
{
|
||||
"platform": "device",
|
||||
"domain": DOMAIN,
|
||||
"device_id": device.id,
|
||||
"type": "set_config_parameter",
|
||||
"parameter": 2,
|
||||
"bitmask": None,
|
||||
"subtype": f"{node.node_id}-112-0-2 (HVAC Settings)",
|
||||
},
|
||||
)
|
||||
assert not capabilities
|
||||
|
||||
|
||||
async def test_get_action_capabilities_lock_triggers(
|
||||
hass: HomeAssistant,
|
||||
client: Client,
|
||||
lock_schlage_be469: Node,
|
||||
integration: ConfigEntry,
|
||||
):
|
||||
"""Test we get the expected action capabilities for lock triggers."""
|
||||
dev_reg = device_registry.async_get(hass)
|
||||
device = device_registry.async_entries_for_config_entry(
|
||||
dev_reg, integration.entry_id
|
||||
)[0]
|
||||
|
||||
# Test clear_lock_usercode
|
||||
capabilities = await device_action.async_get_action_capabilities(
|
||||
hass,
|
||||
{
|
||||
"platform": "device",
|
||||
"domain": DOMAIN,
|
||||
"device_id": device.id,
|
||||
"entity_id": "lock.touchscreen_deadbolt",
|
||||
"type": "clear_lock_usercode",
|
||||
},
|
||||
)
|
||||
assert capabilities and "extra_fields" in capabilities
|
||||
|
||||
assert voluptuous_serialize.convert(
|
||||
capabilities["extra_fields"], custom_serializer=cv.custom_serializer
|
||||
) == [{"type": "string", "name": "code_slot", "required": True}]
|
||||
|
||||
# Test set_lock_usercode
|
||||
capabilities = await device_action.async_get_action_capabilities(
|
||||
hass,
|
||||
{
|
||||
"platform": "device",
|
||||
"domain": DOMAIN,
|
||||
"device_id": device.id,
|
||||
"entity_id": "lock.touchscreen_deadbolt",
|
||||
"type": "set_lock_usercode",
|
||||
},
|
||||
)
|
||||
assert capabilities and "extra_fields" in capabilities
|
||||
|
||||
assert voluptuous_serialize.convert(
|
||||
capabilities["extra_fields"], custom_serializer=cv.custom_serializer
|
||||
) == [
|
||||
{"type": "string", "name": "code_slot", "required": True},
|
||||
{"type": "string", "name": "usercode", "required": True},
|
||||
]
|
||||
|
||||
|
||||
async def test_get_action_capabilities_meter_triggers(
|
||||
hass: HomeAssistant,
|
||||
client: Client,
|
||||
aeon_smart_switch_6: Node,
|
||||
integration: ConfigEntry,
|
||||
) -> None:
|
||||
"""Test we get the expected action capabilities for meter triggers."""
|
||||
node = aeon_smart_switch_6
|
||||
dev_reg = device_registry.async_get(hass)
|
||||
device = dev_reg.async_get_device({get_device_id(client, node)})
|
||||
assert device
|
||||
capabilities = await device_action.async_get_action_capabilities(
|
||||
hass,
|
||||
{
|
||||
"platform": "device",
|
||||
"domain": DOMAIN,
|
||||
"device_id": device.id,
|
||||
"entity_id": "sensor.meter",
|
||||
"type": "reset_meter",
|
||||
},
|
||||
)
|
||||
assert capabilities and "extra_fields" in capabilities
|
||||
|
||||
assert voluptuous_serialize.convert(
|
||||
capabilities["extra_fields"], custom_serializer=cv.custom_serializer
|
||||
) == [{"type": "string", "name": "value", "optional": True}]
|
||||
|
||||
|
||||
async def test_failure_scenarios(
|
||||
hass: HomeAssistant,
|
||||
client: Client,
|
||||
hank_binary_switch: Node,
|
||||
integration: ConfigEntry,
|
||||
):
|
||||
"""Test failure scenarios."""
|
||||
dev_reg = device_registry.async_get(hass)
|
||||
device = device_registry.async_entries_for_config_entry(
|
||||
dev_reg, integration.entry_id
|
||||
)[0]
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await device_action.async_call_action_from_config(
|
||||
hass, {"type": "failed.test", "device_id": device.id}, {}, None
|
||||
)
|
||||
|
||||
assert (
|
||||
await device_action.async_get_action_capabilities(
|
||||
hass, {"type": "failed.test", "device_id": device.id}
|
||||
)
|
||||
== {}
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue