Add zwave_js device triggers for any zwave value (#54958)

* Add zwave_js device triggers for any zwave value

* translations

* Validate value
This commit is contained in:
Raman Gupta 2021-08-21 00:09:52 -04:00 committed by GitHub
parent 1075a65bbd
commit 2be50eb5b4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 520 additions and 24 deletions

View file

@ -2,7 +2,7 @@
from __future__ import annotations
import voluptuous as vol
from zwave_js_server.const import CommandClass
from zwave_js_server.const import CommandClass, ConfigurationValueType
from homeassistant.components.automation import AutomationActionType
from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
@ -23,6 +23,7 @@ from homeassistant.helpers import (
)
from homeassistant.helpers.typing import ConfigType
from . import trigger
from .const import (
ATTR_COMMAND_CLASS,
ATTR_DATA_TYPE,
@ -45,6 +46,7 @@ from .helpers import (
async_get_node_status_sensor_entity_id,
get_zwave_value_from_config,
)
from .triggers.value_updated import ATTR_FROM, ATTR_TO
CONF_SUBTYPE = "subtype"
CONF_VALUE_ID = "value_id"
@ -55,8 +57,18 @@ NOTIFICATION_NOTIFICATION = "event.notification.notification"
BASIC_VALUE_NOTIFICATION = "event.value_notification.basic"
CENTRAL_SCENE_VALUE_NOTIFICATION = "event.value_notification.central_scene"
SCENE_ACTIVATION_VALUE_NOTIFICATION = "event.value_notification.scene_activation"
CONFIG_PARAMETER_VALUE_UPDATED = f"{DOMAIN}.value_updated.config_parameter"
VALUE_VALUE_UPDATED = f"{DOMAIN}.value_updated.value"
NODE_STATUS = "state.node_status"
VALUE_SCHEMA = vol.Any(
bool,
vol.Coerce(int),
vol.Coerce(float),
cv.boolean,
cv.string,
)
NOTIFICATION_EVENT_CC_MAPPINGS = (
(ENTRY_CONTROL_NOTIFICATION, CommandClass.ENTRY_CONTROL),
(NOTIFICATION_NOTIFICATION, CommandClass.NOTIFICATION),
@ -135,12 +147,39 @@ NODE_STATUS_SCHEMA = BASE_STATE_SCHEMA.extend(
}
)
# zwave_js.value_updated based trigger schemas
BASE_VALUE_UPDATED_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
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(None, vol.Coerce(int), str),
vol.Optional(ATTR_ENDPOINT): vol.Any(None, vol.Coerce(int)),
vol.Optional(ATTR_FROM): VALUE_SCHEMA,
vol.Optional(ATTR_TO): VALUE_SCHEMA,
}
)
CONFIG_PARAMETER_VALUE_UPDATED_SCHEMA = BASE_VALUE_UPDATED_SCHEMA.extend(
{
vol.Required(CONF_TYPE): CONFIG_PARAMETER_VALUE_UPDATED,
vol.Required(CONF_SUBTYPE): cv.string,
}
)
VALUE_VALUE_UPDATED_SCHEMA = BASE_VALUE_UPDATED_SCHEMA.extend(
{
vol.Required(CONF_TYPE): VALUE_VALUE_UPDATED,
}
)
TRIGGER_SCHEMA = vol.Any(
ENTRY_CONTROL_NOTIFICATION_SCHEMA,
NOTIFICATION_NOTIFICATION_SCHEMA,
BASIC_VALUE_NOTIFICATION_SCHEMA,
CENTRAL_SCENE_VALUE_NOTIFICATION_SCHEMA,
SCENE_ACTIVATION_VALUE_NOTIFICATION_SCHEMA,
CONFIG_PARAMETER_VALUE_UPDATED_SCHEMA,
VALUE_VALUE_UPDATED_SCHEMA,
NODE_STATUS_SCHEMA,
)
@ -233,6 +272,25 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
]
)
# Generic value update event trigger
triggers.append({**base_trigger, CONF_TYPE: VALUE_VALUE_UPDATED})
# Config parameter value update event triggers
triggers.extend(
[
{
**base_trigger,
CONF_TYPE: CONFIG_PARAMETER_VALUE_UPDATED,
ATTR_PROPERTY: config_value.property_,
ATTR_PROPERTY_KEY: config_value.property_key,
ATTR_ENDPOINT: config_value.endpoint,
ATTR_COMMAND_CLASS: config_value.command_class,
CONF_SUBTYPE: f"{config_value.value_id} ({config_value.property_name})",
}
for config_value in node.get_configuration_values().values()
]
)
return triggers
@ -253,20 +311,25 @@ async def async_attach_trigger(
) -> CALLBACK_TYPE:
"""Attach a trigger."""
trigger_type = config[CONF_TYPE]
trigger_platform = trigger_type.split(".")[0]
event_data = {CONF_DEVICE_ID: config[CONF_DEVICE_ID]}
event_config = {
event.CONF_PLATFORM: "event",
event.CONF_EVENT_DATA: event_data,
}
if ATTR_COMMAND_CLASS in config:
event_data[ATTR_COMMAND_CLASS] = config[ATTR_COMMAND_CLASS]
trigger_split = trigger_type.split(".")
# Our convention for trigger types is to have the trigger type at the beginning
# delimited by a `.`. For zwave_js triggers, there is a `.` in the name
trigger_platform = trigger_split[0]
if trigger_platform == DOMAIN:
trigger_platform = ".".join(trigger_split[:2])
# Take input data from automation trigger UI and add it to the trigger we are
# attaching to
if trigger_platform == "event":
event_data = {CONF_DEVICE_ID: config[CONF_DEVICE_ID]}
event_config = {
event.CONF_PLATFORM: "event",
event.CONF_EVENT_DATA: event_data,
}
if ATTR_COMMAND_CLASS in config:
event_data[ATTR_COMMAND_CLASS] = config[ATTR_COMMAND_CLASS]
if trigger_type == ENTRY_CONTROL_NOTIFICATION:
event_config[event.CONF_EVENT_TYPE] = ZWAVE_JS_NOTIFICATION_EVENT
copy_available_params(config, event_data, [ATTR_EVENT_TYPE, ATTR_DATA_TYPE])
@ -296,19 +359,53 @@ async def async_attach_trigger(
hass, event_config, action, automation_info, platform_type="device"
)
state_config = {state.CONF_PLATFORM: "state"}
if trigger_platform == "state":
if trigger_type == NODE_STATUS:
state_config = {state.CONF_PLATFORM: "state"}
if trigger_platform == "state" and trigger_type == NODE_STATUS:
state_config[state.CONF_ENTITY_ID] = config[CONF_ENTITY_ID]
copy_available_params(
config, state_config, [state.CONF_FOR, state.CONF_FROM, state.CONF_TO]
)
state_config[state.CONF_ENTITY_ID] = config[CONF_ENTITY_ID]
copy_available_params(
config, state_config, [state.CONF_FOR, state.CONF_FROM, state.CONF_TO]
)
else:
raise HomeAssistantError(f"Unhandled trigger type {trigger_type}")
state_config = state.TRIGGER_SCHEMA(state_config)
return await state.async_attach_trigger(
hass, state_config, action, automation_info, platform_type="device"
)
if trigger_platform == f"{DOMAIN}.value_updated":
# Try to get the value to make sure the value ID is valid
try:
node = async_get_node_from_device_id(hass, config[CONF_DEVICE_ID])
get_zwave_value_from_config(node, config)
except (ValueError, vol.Invalid) as err:
raise HomeAssistantError("Invalid value specified") from err
zwave_js_config = {
state.CONF_PLATFORM: trigger_platform,
CONF_DEVICE_ID: config[CONF_DEVICE_ID],
}
copy_available_params(
config,
zwave_js_config,
[
ATTR_COMMAND_CLASS,
ATTR_PROPERTY,
ATTR_PROPERTY_KEY,
ATTR_ENDPOINT,
ATTR_FROM,
ATTR_TO,
],
)
zwave_js_config = await trigger.async_validate_trigger_config(
hass, zwave_js_config
)
return await trigger.async_attach_trigger(
hass, zwave_js_config, action, automation_info
)
raise HomeAssistantError(f"Unhandled trigger type {trigger_type}")
@ -316,12 +413,14 @@ async def async_get_trigger_capabilities(
hass: HomeAssistant, config: ConfigType
) -> dict[str, vol.Schema]:
"""List trigger capabilities."""
trigger_type = config[CONF_TYPE]
node = async_get_node_from_device_id(hass, config[CONF_DEVICE_ID])
value = (
get_zwave_value_from_config(node, config) if ATTR_PROPERTY in config else None
)
# Add additional fields to the automation trigger UI
if config[CONF_TYPE] == NOTIFICATION_NOTIFICATION:
if trigger_type == NOTIFICATION_NOTIFICATION:
return {
"extra_fields": vol.Schema(
{
@ -333,7 +432,7 @@ async def async_get_trigger_capabilities(
)
}
if config[CONF_TYPE] == ENTRY_CONTROL_NOTIFICATION:
if trigger_type == ENTRY_CONTROL_NOTIFICATION:
return {
"extra_fields": vol.Schema(
{
@ -343,7 +442,7 @@ async def async_get_trigger_capabilities(
)
}
if config[CONF_TYPE] == NODE_STATUS:
if trigger_type == NODE_STATUS:
return {
"extra_fields": vol.Schema(
{
@ -354,7 +453,7 @@ async def async_get_trigger_capabilities(
)
}
if config[CONF_TYPE] in (
if trigger_type in (
BASIC_VALUE_NOTIFICATION,
CENTRAL_SCENE_VALUE_NOTIFICATION,
SCENE_ACTIVATION_VALUE_NOTIFICATION,
@ -369,4 +468,47 @@ async def async_get_trigger_capabilities(
return {"extra_fields": vol.Schema({vol.Optional(ATTR_VALUE): value_schema})}
if trigger_type == CONFIG_PARAMETER_VALUE_UPDATED:
# We can be more deliberate about the config parameter schema here because
# there are a limited number of types
if value.configuration_value_type == ConfigurationValueType.UNDEFINED:
return {}
if value.configuration_value_type == ConfigurationValueType.ENUMERATED:
value_schema = vol.In({int(k): v for k, v in value.metadata.states.items()})
else:
value_schema = vol.All(
vol.Coerce(int),
vol.Range(min=value.metadata.min, max=value.metadata.max),
)
return {
"extra_fields": vol.Schema(
{
vol.Optional(state.CONF_FROM): value_schema,
vol.Optional(state.CONF_TO): value_schema,
}
)
}
if trigger_type == VALUE_VALUE_UPDATED:
# Only show command classes on this node and exclude Configuration CC since it
# is already covered
return {
"extra_fields": vol.Schema(
{
vol.Required(ATTR_COMMAND_CLASS): vol.In(
{
CommandClass(cc.id).value: CommandClass(cc.id).name
for cc in sorted(node.command_classes, key=lambda cc: cc.name) # type: ignore[no-any-return]
if cc.id != CommandClass.CONFIGURATION
}
),
vol.Required(ATTR_PROPERTY): cv.string,
vol.Optional(ATTR_PROPERTY_KEY): cv.string,
vol.Optional(ATTR_ENDPOINT): cv.string,
vol.Optional(state.CONF_FROM): cv.string,
vol.Optional(state.CONF_TO): cv.string,
}
)
}
return {}