core/homeassistant/components/zwave_js/device_condition.py

237 lines
7.7 KiB
Python

"""Provide the device conditions for Z-Wave JS."""
from __future__ import annotations
from typing import cast
import voluptuous as vol
from zwave_js_server.const import CommandClass
from zwave_js_server.model.value import ConfigurationValue
from homeassistant.components.device_automation.exceptions import (
InvalidDeviceAutomationConfig,
)
from homeassistant.const import CONF_CONDITION, CONF_DEVICE_ID, CONF_DOMAIN, CONF_TYPE
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import condition, config_validation as cv
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from . import DOMAIN
from .const import (
ATTR_COMMAND_CLASS,
ATTR_ENDPOINT,
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_zwave_value_from_config,
remove_keys_with_empty_values,
)
CONF_STATUS = "status"
NODE_STATUS_TYPE = "node_status"
CONFIG_PARAMETER_TYPE = "config_parameter"
VALUE_TYPE = "value"
CONDITION_TYPES = {NODE_STATUS_TYPE, CONFIG_PARAMETER_TYPE, VALUE_TYPE}
NODE_STATUS_CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend(
{
vol.Required(CONF_TYPE): NODE_STATUS_TYPE,
vol.Required(CONF_STATUS): vol.In(NODE_STATUSES),
}
)
CONFIG_PARAMETER_CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend(
{
vol.Required(CONF_TYPE): CONFIG_PARAMETER_TYPE,
vol.Required(CONF_VALUE_ID): cv.string,
vol.Required(CONF_SUBTYPE): cv.string,
vol.Optional(ATTR_VALUE): vol.Coerce(int),
}
)
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): VALUE_SCHEMA,
}
)
TYPE_SCHEMA_MAP = {
NODE_STATUS_TYPE: NODE_STATUS_CONDITION_SCHEMA,
CONFIG_PARAMETER_TYPE: CONFIG_PARAMETER_CONDITION_SCHEMA,
VALUE_TYPE: VALUE_CONDITION_SCHEMA,
}
CONDITION_TYPE_SCHEMA = vol.Schema(
{vol.Required(CONF_TYPE): vol.In(TYPE_SCHEMA_MAP)}, extra=vol.ALLOW_EXTRA
)
CONDITION_SCHEMA = vol.All(
remove_keys_with_empty_values,
CONDITION_TYPE_SCHEMA,
check_type_schema_map(TYPE_SCHEMA_MAP),
)
async def async_validate_condition_config(
hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
config = CONDITION_SCHEMA(config)
# We return early if the config entry for this device is not ready because we can't
# validate the value without knowing the state of the device
if async_is_device_config_entry_not_loaded(hass, config[CONF_DEVICE_ID]):
return config
if config[CONF_TYPE] == VALUE_TYPE:
try:
node = async_get_node_from_device_id(hass, config[CONF_DEVICE_ID])
get_zwave_value_from_config(node, config)
except vol.Invalid as err:
raise InvalidDeviceAutomationConfig(err.msg) from err
return config
async def async_get_conditions(
hass: HomeAssistant, device_id: str
) -> list[dict[str, str]]:
"""List device conditions for Z-Wave JS devices."""
conditions = []
base_condition = {
CONF_CONDITION: "device",
CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN,
}
node = async_get_node_from_device_id(hass, device_id)
# Any value's value condition
conditions.append({**base_condition, CONF_TYPE: VALUE_TYPE})
# Node status conditions
conditions.append({**base_condition, CONF_TYPE: NODE_STATUS_TYPE})
# Config parameter conditions
conditions.extend(
[
{
**base_condition,
CONF_VALUE_ID: config_value.value_id,
CONF_TYPE: CONFIG_PARAMETER_TYPE,
CONF_SUBTYPE: f"{config_value.value_id} ({config_value.property_name})",
}
for config_value in node.get_configuration_values().values()
]
)
return conditions
@callback
def async_condition_from_config(
config: ConfigType, config_validation: bool
) -> condition.ConditionCheckerType:
"""Create a function to test a device condition."""
if config_validation:
config = CONDITION_SCHEMA(config)
condition_type = config[CONF_TYPE]
device_id = config[CONF_DEVICE_ID]
@callback
def test_node_status(hass: HomeAssistant, variables: TemplateVarsType) -> bool:
"""Test if node status is a certain state."""
node = async_get_node_from_device_id(hass, device_id)
return bool(node.status.name.lower() == config[CONF_STATUS])
if condition_type == NODE_STATUS_TYPE:
return test_node_status
@callback
def test_config_parameter(hass: HomeAssistant, variables: TemplateVarsType) -> bool:
"""Test if config parameter is a certain state."""
node = async_get_node_from_device_id(hass, device_id)
config_value = cast(ConfigurationValue, node.values[config[CONF_VALUE_ID]])
return bool(config_value.value == config[ATTR_VALUE])
if condition_type == CONFIG_PARAMETER_TYPE:
return test_config_parameter
@callback
def test_value(hass: HomeAssistant, variables: TemplateVarsType) -> bool:
"""Test if value is a certain state."""
node = async_get_node_from_device_id(hass, device_id)
value = get_zwave_value_from_config(node, config)
return bool(value.value == config[ATTR_VALUE])
if condition_type == VALUE_TYPE:
return test_value
raise HomeAssistantError(f"Unhandled condition type {condition_type}")
@callback
async def async_get_condition_capabilities(
hass: HomeAssistant, config: ConfigType
) -> dict[str, vol.Schema]:
"""List condition capabilities."""
device_id = config[CONF_DEVICE_ID]
node = async_get_node_from_device_id(hass, device_id)
# Add additional fields to the automation trigger UI
if config[CONF_TYPE] == CONFIG_PARAMETER_TYPE:
value_id = config[CONF_VALUE_ID]
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:
# 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: cc.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.Required(ATTR_VALUE): cv.string,
}
)
}
if config[CONF_TYPE] == NODE_STATUS_TYPE:
return {
"extra_fields": vol.Schema(
{vol.Required(CONF_STATUS): vol.In(NODE_STATUSES)}
)
}
return {}