diff --git a/homeassistant/components/zwave_js/device_trigger.py b/homeassistant/components/zwave_js/device_trigger.py new file mode 100644 index 00000000000..6734afd10e2 --- /dev/null +++ b/homeassistant/components/zwave_js/device_trigger.py @@ -0,0 +1,371 @@ +"""Provides device triggers for Z-Wave JS.""" +from __future__ import annotations + +import voluptuous as vol +from zwave_js_server.const import CommandClass + +from homeassistant.components.automation import AutomationActionType +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA +from homeassistant.components.homeassistant.triggers import event, state +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, +) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import ( + config_validation as cv, + device_registry, + entity_registry, +) +from homeassistant.helpers.typing import ConfigType + +from .const import ( + ATTR_COMMAND_CLASS, + ATTR_DATA_TYPE, + ATTR_ENDPOINT, + ATTR_EVENT, + ATTR_EVENT_LABEL, + ATTR_EVENT_TYPE, + ATTR_LABEL, + ATTR_PROPERTY, + ATTR_PROPERTY_KEY, + ATTR_TYPE, + ATTR_VALUE, + ATTR_VALUE_RAW, + DOMAIN, + ZWAVE_JS_NOTIFICATION_EVENT, + ZWAVE_JS_VALUE_NOTIFICATION_EVENT, +) +from .helpers import ( + async_get_node_from_device_id, + async_get_node_status_sensor_entity_id, + get_zwave_value_from_config, +) + +CONF_SUBTYPE = "subtype" +CONF_VALUE_ID = "value_id" + +# Trigger types +ENTRY_CONTROL_NOTIFICATION = "event.notification.entry_control" +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" +NODE_STATUS = "state.node_status" + +NOTIFICATION_EVENT_CC_MAPPINGS = ( + (ENTRY_CONTROL_NOTIFICATION, CommandClass.ENTRY_CONTROL), + (NOTIFICATION_NOTIFICATION, CommandClass.NOTIFICATION), +) + +# Event based trigger schemas +BASE_EVENT_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(ATTR_COMMAND_CLASS): vol.In([cc.value for cc in CommandClass]), + } +) + +NOTIFICATION_NOTIFICATION_SCHEMA = BASE_EVENT_SCHEMA.extend( + { + vol.Required(CONF_TYPE): NOTIFICATION_NOTIFICATION, + vol.Optional(f"{ATTR_TYPE}."): vol.Coerce(int), + vol.Optional(ATTR_LABEL): cv.string, + vol.Optional(ATTR_EVENT): vol.Coerce(int), + vol.Optional(ATTR_EVENT_LABEL): cv.string, + } +) + +ENTRY_CONTROL_NOTIFICATION_SCHEMA = BASE_EVENT_SCHEMA.extend( + { + vol.Required(CONF_TYPE): ENTRY_CONTROL_NOTIFICATION, + vol.Optional(ATTR_EVENT_TYPE): vol.Coerce(int), + vol.Optional(ATTR_DATA_TYPE): vol.Coerce(int), + } +) + +BASE_VALUE_NOTIFICATION_EVENT_SCHEMA = BASE_EVENT_SCHEMA.extend( + { + vol.Required(ATTR_PROPERTY): vol.Any(int, str), + vol.Required(ATTR_PROPERTY_KEY): vol.Any(None, int, str), + vol.Required(ATTR_ENDPOINT): vol.Coerce(int), + vol.Required(ATTR_VALUE): vol.Coerce(int), + vol.Required(CONF_SUBTYPE): cv.string, + } +) + +BASIC_VALUE_NOTIFICATION_SCHEMA = BASE_VALUE_NOTIFICATION_EVENT_SCHEMA.extend( + { + vol.Required(CONF_TYPE): BASIC_VALUE_NOTIFICATION, + } +) + +CENTRAL_SCENE_VALUE_NOTIFICATION_SCHEMA = BASE_VALUE_NOTIFICATION_EVENT_SCHEMA.extend( + { + vol.Required(CONF_TYPE): CENTRAL_SCENE_VALUE_NOTIFICATION, + } +) + +SCENE_ACTIVATION_VALUE_NOTIFICATION_SCHEMA = ( + BASE_VALUE_NOTIFICATION_EVENT_SCHEMA.extend( + { + vol.Required(CONF_TYPE): SCENE_ACTIVATION_VALUE_NOTIFICATION, + } + ) +) + +# State based trigger schemas +BASE_STATE_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + } +) + +NODE_STATUSES = ["asleep", "awake", "dead", "alive"] + +NODE_STATUS_SCHEMA = BASE_STATE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): NODE_STATUS, + vol.Optional(state.CONF_FROM): vol.In(NODE_STATUSES), + vol.Optional(state.CONF_TO): vol.In(NODE_STATUSES), + vol.Optional(state.CONF_FOR): cv.positive_time_period_dict, + } +) + +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, + NODE_STATUS_SCHEMA, +) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: + """List device triggers for Z-Wave JS devices.""" + dev_reg = device_registry.async_get(hass) + node = async_get_node_from_device_id(hass, device_id, dev_reg) + + triggers = [] + base_trigger = { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + } + + # We can add a node status trigger if the node status sensor is enabled + ent_reg = entity_registry.async_get(hass) + entity_id = async_get_node_status_sensor_entity_id( + hass, device_id, ent_reg, dev_reg + ) + if (entity := ent_reg.async_get(entity_id)) is not None and not entity.disabled: + triggers.append( + {**base_trigger, CONF_TYPE: NODE_STATUS, CONF_ENTITY_ID: entity_id} + ) + + # Handle notification event triggers + triggers.extend( + [ + {**base_trigger, CONF_TYPE: event_type, ATTR_COMMAND_CLASS: command_class} + for event_type, command_class in NOTIFICATION_EVENT_CC_MAPPINGS + if any(cc.id == command_class for cc in node.command_classes) + ] + ) + + # Handle central scene value notification event triggers + triggers.extend( + [ + { + **base_trigger, + CONF_TYPE: CENTRAL_SCENE_VALUE_NOTIFICATION, + ATTR_PROPERTY: value.property_, + ATTR_PROPERTY_KEY: value.property_key, + ATTR_ENDPOINT: value.endpoint, + ATTR_COMMAND_CLASS: CommandClass.CENTRAL_SCENE, + CONF_SUBTYPE: f"Endpoint {value.endpoint} Scene {value.property_key}", + } + for value in node.get_command_class_values( + CommandClass.CENTRAL_SCENE + ).values() + if value.property_ == "scene" + ] + ) + + # Handle scene activation value notification event triggers + triggers.extend( + [ + { + **base_trigger, + CONF_TYPE: SCENE_ACTIVATION_VALUE_NOTIFICATION, + ATTR_PROPERTY: value.property_, + ATTR_PROPERTY_KEY: value.property_key, + ATTR_ENDPOINT: value.endpoint, + ATTR_COMMAND_CLASS: CommandClass.SCENE_ACTIVATION, + CONF_SUBTYPE: f"Endpoint {value.endpoint}", + } + for value in node.get_command_class_values( + CommandClass.SCENE_ACTIVATION + ).values() + if value.property_ == "sceneId" + ] + ) + + # Handle basic value notification event triggers + # Nodes will only send Basic CC value notifications if a compatibility flag is set + if node.device_config.compat.get("treatBasicSetAsEvent", False): + triggers.extend( + [ + { + **base_trigger, + CONF_TYPE: BASIC_VALUE_NOTIFICATION, + ATTR_PROPERTY: value.property_, + ATTR_PROPERTY_KEY: value.property_key, + ATTR_ENDPOINT: value.endpoint, + ATTR_COMMAND_CLASS: CommandClass.BASIC, + CONF_SUBTYPE: f"Endpoint {value.endpoint}", + } + for value in node.get_command_class_values(CommandClass.BASIC).values() + if value.property_ == "event" + ] + ) + + return triggers + + +def copy_available_params( + input_dict: dict, output_dict: dict, params: list[str] +) -> None: + """Copy available params from input into output.""" + for param in params: + if (val := input_dict.get(param)) not in ("", None): + output_dict[param] = val + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> 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] + + # Take input data from automation trigger UI and add it to the trigger we are + # attaching to + if trigger_platform == "event": + 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]) + elif trigger_type == NOTIFICATION_NOTIFICATION: + event_config[event.CONF_EVENT_TYPE] = ZWAVE_JS_NOTIFICATION_EVENT + copy_available_params( + config, event_data, [ATTR_LABEL, ATTR_EVENT_LABEL, ATTR_EVENT] + ) + if (val := config.get(f"{ATTR_TYPE}.")) not in ("", None): + event_data[ATTR_TYPE] = val + elif trigger_type in ( + BASIC_VALUE_NOTIFICATION, + CENTRAL_SCENE_VALUE_NOTIFICATION, + SCENE_ACTIVATION_VALUE_NOTIFICATION, + ): + event_config[event.CONF_EVENT_TYPE] = ZWAVE_JS_VALUE_NOTIFICATION_EVENT + copy_available_params( + config, event_data, [ATTR_PROPERTY, ATTR_PROPERTY_KEY, ATTR_ENDPOINT] + ) + event_data[ATTR_VALUE_RAW] = config[ATTR_VALUE] + else: + raise HomeAssistantError(f"Unhandled trigger type {trigger_type}") + + event_config = event.TRIGGER_SCHEMA(event_config) + return await event.async_attach_trigger( + hass, event_config, action, automation_info, platform_type="device" + ) + + 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.TRIGGER_SCHEMA(state_config) + return await state.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) + + raise HomeAssistantError(f"Unhandled trigger type {trigger_type}") + + +async def async_get_trigger_capabilities( + hass: HomeAssistant, config: ConfigType +) -> dict[str, vol.Schema]: + """List trigger capabilities.""" + 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: + return { + "extra_fields": vol.Schema( + { + vol.Optional(f"{ATTR_TYPE}."): cv.string, + vol.Optional(ATTR_LABEL): cv.string, + vol.Optional(ATTR_EVENT): cv.string, + vol.Optional(ATTR_EVENT_LABEL): cv.string, + } + ) + } + + if config[CONF_TYPE] == ENTRY_CONTROL_NOTIFICATION: + return { + "extra_fields": vol.Schema( + { + vol.Optional(ATTR_EVENT_TYPE): cv.string, + vol.Optional(ATTR_DATA_TYPE): cv.string, + } + ) + } + + if config[CONF_TYPE] == NODE_STATUS: + return { + "extra_fields": vol.Schema( + { + vol.Optional(state.CONF_FROM): vol.In(NODE_STATUSES), + vol.Optional(state.CONF_TO): vol.In(NODE_STATUSES), + vol.Optional(state.CONF_FOR): cv.positive_time_period_dict, + } + ) + } + + if config[CONF_TYPE] in ( + BASIC_VALUE_NOTIFICATION, + CENTRAL_SCENE_VALUE_NOTIFICATION, + SCENE_ACTIVATION_VALUE_NOTIFICATION, + ): + if value.metadata.states: + 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.Required(ATTR_VALUE): value_schema})} + + return {} diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index 50d57d9a6e4..593d5ea4151 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -8,9 +8,11 @@ from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.value import Value as ZwaveValue, get_value_id +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import __version__ as HA_VERSION from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import ( DeviceRegistry, async_get as async_get_dev_reg, @@ -175,3 +177,35 @@ def get_zwave_value_from_config(node: ZwaveNode, config: ConfigType) -> ZwaveVal if value_id not in node.values: raise vol.Invalid(f"Value {value_id} can't be found on node {node}") return node.values[value_id] + + +@callback +def async_get_node_status_sensor_entity_id( + hass: HomeAssistant, + device_id: str, + ent_reg: EntityRegistry | None = None, + dev_reg: DeviceRegistry | None = None, +) -> str: + """Get the node status sensor entity ID for a given Z-Wave JS device.""" + if not ent_reg: + ent_reg = async_get_ent_reg(hass) + if not dev_reg: + dev_reg = async_get_dev_reg(hass) + device = dev_reg.async_get(device_id) + if not device: + raise HomeAssistantError("Invalid Device ID provided") + + entry_id = next(entry_id for entry_id in device.config_entries) + client = hass.data[DOMAIN][entry_id][DATA_CLIENT] + node = async_get_node_from_device_id(hass, device_id, dev_reg) + entity_id = ent_reg.async_get_entity_id( + SENSOR_DOMAIN, + DOMAIN, + f"{client.driver.controller.home_id}.{node.node_id}.node_status", + ) + if not entity_id: + raise HomeAssistantError( + "Node status sensor entity not found. Device may not be a zwave_js device" + ) + + return entity_id diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 1595ee58889..3d5aa277943 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -99,6 +99,14 @@ } }, "device_automation": { + "trigger_type": { + "event.notification.entry_control": "Sent an Entry Control notification", + "event.notification.notification": "Sent a notification", + "event.value_notification.basic": "Basic CC event on {subtype}", + "event.value_notification.central_scene": "Central Scene action on {subtype}", + "event.value_notification.scene_activation": "Scene Activation on {subtype}", + "state.node_status": "Node status changed" + }, "condition_type": { "node_status": "Node status", "config_parameter": "Config parameter {subtype} value", diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json index bec90dd50d8..b742a011d19 100644 --- a/homeassistant/components/zwave_js/translations/en.json +++ b/homeassistant/components/zwave_js/translations/en.json @@ -56,6 +56,14 @@ "config_parameter": "Config parameter {subtype} value", "node_status": "Node status", "value": "Current value of a Z-Wave Value" + }, + "trigger_type": { + "event.notification.entry_control": "Sent an Entry Control notification", + "event.notification.notification": "Sent a notification", + "event.value_notification.basic": "Basic CC event on {subtype}", + "event.value_notification.central_scene": "Central Scene action on {subtype}", + "event.value_notification.scene_activation": "Scene Activation on {subtype}", + "state.node_status": "Node status changed" } }, "options": { diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index f62c7fb8b9f..02d5b10cbba 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -429,6 +429,12 @@ def wallmote_central_scene_state_fixture(): return json.loads(load_fixture("zwave_js/wallmote_central_scene_state.json")) +@pytest.fixture(name="ge_in_wall_dimmer_switch_state", scope="session") +def ge_in_wall_dimmer_switch_state_fixture(): + """Load the ge in-wall dimmer switch node state fixture data.""" + return json.loads(load_fixture("zwave_js/ge_in_wall_dimmer_switch_state.json")) + + @pytest.fixture(name="aeotec_zw164_siren_state", scope="session") def aeotec_zw164_siren_state_fixture(): """Load the aeotec zw164 siren node state fixture data.""" @@ -795,6 +801,14 @@ def wallmote_central_scene_fixture(client, wallmote_central_scene_state): return node +@pytest.fixture(name="ge_in_wall_dimmer_switch") +def ge_in_wall_dimmer_switch_fixture(client, ge_in_wall_dimmer_switch_state): + """Mock a ge in-wall dimmer switch scene node.""" + node = Node(client, copy.deepcopy(ge_in_wall_dimmer_switch_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="aeotec_zw164_siren") def aeotec_zw164_siren_fixture(client, aeotec_zw164_siren_state): """Mock a wallmote central scene node.""" diff --git a/tests/components/zwave_js/test_device_trigger.py b/tests/components/zwave_js/test_device_trigger.py new file mode 100644 index 00000000000..2c4d8ce2b33 --- /dev/null +++ b/tests/components/zwave_js/test_device_trigger.py @@ -0,0 +1,831 @@ +"""The tests for Z-Wave JS device triggers.""" +from unittest.mock import patch + +import pytest +import voluptuous_serialize +from zwave_js_server.const import CommandClass +from zwave_js_server.event import Event +from zwave_js_server.model.node import Node + +from homeassistant.components import automation +from homeassistant.components.zwave_js import DOMAIN, device_trigger +from homeassistant.components.zwave_js.device_trigger import ( + async_attach_trigger, + async_get_trigger_capabilities, +) +from homeassistant.components.zwave_js.helpers import ( + async_get_node_status_sensor_entity_id, +) +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.device_registry import ( + async_entries_for_config_entry, + async_get as async_get_dev_reg, +) +from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg +from homeassistant.setup import async_setup_component + +from tests.common import ( + assert_lists_same, + async_get_device_automations, + async_mock_service, +) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_notification_notification_triggers( + hass, client, lock_schlage_be469, integration +): + """Test we get the expected triggers from a zwave_js device with the Notification CC.""" + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + expected_trigger = { + "platform": "device", + "domain": DOMAIN, + "type": "event.notification.notification", + "device_id": device.id, + "command_class": CommandClass.NOTIFICATION, + } + triggers = await async_get_device_automations(hass, "trigger", device.id) + assert expected_trigger in triggers + + +async def test_if_notification_notification_fires( + hass, client, lock_schlage_be469, integration, calls +): + """Test for event.notification.notification trigger firing.""" + node: Node = lock_schlage_be469 + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "event.notification.notification", + "command_class": CommandClass.NOTIFICATION.value, + "type.": 6, + "event": 5, + "label": "Access Control", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "event.notification.notification - " + "{{ trigger.platform}} - " + "{{ trigger.event.event_type}} - " + "{{ trigger.event.data.command_class }}" + ) + }, + }, + }, + ] + }, + ) + + # Publish fake Notification CC notification + event = Event( + type="notification", + data={ + "source": "node", + "event": "notification", + "nodeId": node.node_id, + "ccId": 113, + "args": { + "type": 6, + "event": 5, + "label": "Access Control", + "eventLabel": "Keypad lock operation", + "parameters": {"userId": 1}, + }, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data[ + "some" + ] == "event.notification.notification - device - zwave_js_notification - {}".format( + CommandClass.NOTIFICATION + ) + + +async def test_get_trigger_capabilities_notification_notification( + hass, client, lock_schlage_be469, integration +): + """Test we get the expected capabilities from a notification.notification trigger.""" + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + capabilities = await device_trigger.async_get_trigger_capabilities( + hass, + { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "event.notification.notification", + "command_class": CommandClass.NOTIFICATION.value, + }, + ) + assert capabilities and "extra_fields" in capabilities + + assert_lists_same( + voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ), + [ + {"name": "type.", "optional": True, "type": "string"}, + {"name": "label", "optional": True, "type": "string"}, + {"name": "event", "optional": True, "type": "string"}, + {"name": "event_label", "optional": True, "type": "string"}, + ], + ) + + +async def test_if_entry_control_notification_fires( + hass, client, lock_schlage_be469, integration, calls +): + """Test for notification.entry_control trigger firing.""" + node: Node = lock_schlage_be469 + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "event.notification.entry_control", + "command_class": CommandClass.ENTRY_CONTROL.value, + "event_type": 5, + "data_type": 2, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "event.notification.notification - " + "{{ trigger.platform}} - " + "{{ trigger.event.event_type}} - " + "{{ trigger.event.data.command_class }}" + ) + }, + }, + }, + ] + }, + ) + + # Publish fake Entry Control CC notification + event = Event( + type="notification", + data={ + "source": "node", + "event": "notification", + "nodeId": node.node_id, + "ccId": 111, + "args": {"eventType": 5, "dataType": 2, "eventData": "555"}, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data[ + "some" + ] == "event.notification.notification - device - zwave_js_notification - {}".format( + CommandClass.ENTRY_CONTROL + ) + + +async def test_get_trigger_capabilities_entry_control_notification( + hass, client, lock_schlage_be469, integration +): + """Test we get the expected capabilities from a notification.entry_control trigger.""" + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + capabilities = await device_trigger.async_get_trigger_capabilities( + hass, + { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "event.notification.entry_control", + "command_class": CommandClass.ENTRY_CONTROL.value, + }, + ) + assert capabilities and "extra_fields" in capabilities + + assert_lists_same( + voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ), + [ + {"name": "event_type", "optional": True, "type": "string"}, + {"name": "data_type", "optional": True, "type": "string"}, + ], + ) + + +async def test_get_node_status_triggers(hass, client, lock_schlage_be469, integration): + """Test we get the expected triggers from a device with node status sensor enabled.""" + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + ent_reg = async_get_ent_reg(hass) + entity_id = async_get_node_status_sensor_entity_id( + hass, device.id, ent_reg, dev_reg + ) + ent_reg.async_update_entity(entity_id, **{"disabled_by": None}) + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + expected_trigger = { + "platform": "device", + "domain": DOMAIN, + "type": "state.node_status", + "device_id": device.id, + "entity_id": entity_id, + } + triggers = await async_get_device_automations(hass, "trigger", device.id) + assert expected_trigger in triggers + + +async def test_if_node_status_change_fires( + hass, client, lock_schlage_be469, integration, calls +): + """Test for node_status trigger firing.""" + node: Node = lock_schlage_be469 + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + ent_reg = async_get_ent_reg(hass) + entity_id = async_get_node_status_sensor_entity_id( + hass, device.id, ent_reg, dev_reg + ) + ent_reg.async_update_entity(entity_id, **{"disabled_by": None}) + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "entity_id": entity_id, + "type": "state.node_status", + "from": "alive", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "state.node_status - " + "{{ trigger.platform}} - " + "{{ trigger.from_state.state }}" + ) + }, + }, + }, + ] + }, + ) + + # Test status change + event = Event( + "dead", data={"source": "node", "event": "dead", "nodeId": node.node_id} + ) + node.receive_event(event) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "state.node_status - device - alive" + + +async def test_get_trigger_capabilities_node_status( + hass, client, lock_schlage_be469, integration +): + """Test we get the expected capabilities from a node_status trigger.""" + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + ent_reg = async_get_ent_reg(hass) + entity_id = async_get_node_status_sensor_entity_id( + hass, device.id, ent_reg, dev_reg + ) + ent_reg.async_update_entity(entity_id, **{"disabled_by": None}) + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + capabilities = await device_trigger.async_get_trigger_capabilities( + hass, + { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "entity_id": entity_id, + "type": "state.node_status", + }, + ) + assert capabilities and "extra_fields" in capabilities + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "from", + "optional": True, + "options": [ + ("asleep", "asleep"), + ("awake", "awake"), + ("dead", "dead"), + ("alive", "alive"), + ], + "type": "select", + }, + { + "name": "to", + "optional": True, + "options": [ + ("asleep", "asleep"), + ("awake", "awake"), + ("dead", "dead"), + ("alive", "alive"), + ], + "type": "select", + }, + {"name": "for", "optional": True, "type": "positive_time_period_dict"}, + ] + + +async def test_get_basic_value_notification_triggers( + hass, client, ge_in_wall_dimmer_switch, integration +): + """Test we get the expected triggers from a zwave_js device with the Basic CC.""" + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + expected_trigger = { + "platform": "device", + "domain": DOMAIN, + "type": "event.value_notification.basic", + "device_id": device.id, + "command_class": CommandClass.BASIC, + "property": "event", + "property_key": None, + "endpoint": 0, + "subtype": "Endpoint 0", + } + triggers = await async_get_device_automations(hass, "trigger", device.id) + assert expected_trigger in triggers + + +async def test_if_basic_value_notification_fires( + hass, client, ge_in_wall_dimmer_switch, integration, calls +): + """Test for event.value_notification.basic trigger firing.""" + node: Node = ge_in_wall_dimmer_switch + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "type": "event.value_notification.basic", + "device_id": device.id, + "command_class": CommandClass.BASIC.value, + "property": "event", + "property_key": None, + "endpoint": 0, + "subtype": "Endpoint 0", + "value": 0, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "event.value_notification.basic - " + "{{ trigger.platform}} - " + "{{ trigger.event.event_type}} - " + "{{ trigger.event.data.command_class }}" + ) + }, + }, + }, + ] + }, + ) + + # Publish fake Basic CC value notification + event = Event( + type="value notification", + data={ + "source": "node", + "event": "value notification", + "nodeId": node.node_id, + "args": { + "commandClassName": "Basic", + "commandClass": 32, + "endpoint": 0, + "property": "event", + "propertyName": "event", + "value": 0, + "metadata": { + "type": "number", + "readable": True, + "writeable": False, + "label": "Event value", + "min": 0, + "max": 255, + }, + "ccVersion": 1, + }, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data[ + "some" + ] == "event.value_notification.basic - device - zwave_js_value_notification - {}".format( + CommandClass.BASIC + ) + + +async def test_get_trigger_capabilities_basic_value_notification( + hass, client, ge_in_wall_dimmer_switch, integration +): + """Test we get the expected capabilities from a value_notification.basic trigger.""" + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + capabilities = await device_trigger.async_get_trigger_capabilities( + hass, + { + "platform": "device", + "domain": DOMAIN, + "type": "event.value_notification.basic", + "device_id": device.id, + "command_class": CommandClass.BASIC.value, + "property": "event", + "property_key": None, + "endpoint": 0, + "subtype": "Endpoint 0", + }, + ) + 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": 255, + } + ] + + +async def test_get_central_scene_value_notification_triggers( + hass, client, wallmote_central_scene, integration +): + """Test we get the expected triggers from a zwave_js device with the Central Scene CC.""" + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + expected_trigger = { + "platform": "device", + "domain": DOMAIN, + "type": "event.value_notification.central_scene", + "device_id": device.id, + "command_class": CommandClass.CENTRAL_SCENE, + "property": "scene", + "property_key": "001", + "endpoint": 0, + "subtype": "Endpoint 0 Scene 001", + } + triggers = await async_get_device_automations(hass, "trigger", device.id) + assert expected_trigger in triggers + + +async def test_if_central_scene_value_notification_fires( + hass, client, wallmote_central_scene, integration, calls +): + """Test for event.value_notification.central_scene trigger firing.""" + node: Node = wallmote_central_scene + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "event.value_notification.central_scene", + "command_class": CommandClass.CENTRAL_SCENE.value, + "property": "scene", + "property_key": "001", + "endpoint": 0, + "subtype": "Endpoint 0 Scene 001", + "value": 0, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "event.value_notification.central_scene - " + "{{ trigger.platform}} - " + "{{ trigger.event.event_type}} - " + "{{ trigger.event.data.command_class }}" + ) + }, + }, + }, + ] + }, + ) + + # Publish fake Central Scene CC value notification + event = Event( + type="value notification", + data={ + "source": "node", + "event": "value notification", + "nodeId": node.node_id, + "args": { + "commandClassName": "Central Scene", + "commandClass": 91, + "endpoint": 0, + "property": "scene", + "propertyName": "scene", + "propertyKey": "001", + "propertyKey": "001", + "value": 0, + "metadata": { + "type": "number", + "readable": True, + "writeable": False, + "min": 0, + "max": 255, + "label": "Scene 004", + "states": { + "0": "KeyPressed", + "1": "KeyReleased", + "2": "KeyHeldDown", + }, + }, + "ccVersion": 1, + }, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data[ + "some" + ] == "event.value_notification.central_scene - device - zwave_js_value_notification - {}".format( + CommandClass.CENTRAL_SCENE + ) + + +async def test_get_trigger_capabilities_central_scene_value_notification( + hass, client, wallmote_central_scene, integration +): + """Test we get the expected capabilities from a value_notification.central_scene trigger.""" + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + capabilities = await device_trigger.async_get_trigger_capabilities( + hass, + { + "platform": "device", + "domain": DOMAIN, + "type": "event.value_notification.central_scene", + "device_id": device.id, + "command_class": CommandClass.CENTRAL_SCENE.value, + "property": "scene", + "property_key": "001", + "endpoint": 0, + "subtype": "Endpoint 0 Scene 001", + }, + ) + assert capabilities and "extra_fields" in capabilities + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "value", + "required": True, + "type": "select", + "options": [(0, "KeyPressed"), (1, "KeyReleased"), (2, "KeyHeldDown")], + }, + ] + + +async def test_get_scene_activation_value_notification_triggers( + hass, client, hank_binary_switch, integration +): + """Test we get the expected triggers from a zwave_js device with the SceneActivation CC.""" + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + expected_trigger = { + "platform": "device", + "domain": DOMAIN, + "type": "event.value_notification.scene_activation", + "device_id": device.id, + "command_class": CommandClass.SCENE_ACTIVATION.value, + "property": "sceneId", + "property_key": None, + "endpoint": 0, + "subtype": "Endpoint 0", + } + triggers = await async_get_device_automations(hass, "trigger", device.id) + assert expected_trigger in triggers + + +async def test_if_scene_activation_value_notification_fires( + hass, client, hank_binary_switch, integration, calls +): + """Test for event.value_notification.scene_activation trigger firing.""" + node: Node = hank_binary_switch + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "event.value_notification.scene_activation", + "command_class": CommandClass.SCENE_ACTIVATION.value, + "property": "sceneId", + "property_key": None, + "endpoint": 0, + "subtype": "Endpoint 0", + "value": 1, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "event.value_notification.scene_activation - " + "{{ trigger.platform}} - " + "{{ trigger.event.event_type}} - " + "{{ trigger.event.data.command_class }}" + ) + }, + }, + }, + ] + }, + ) + + # Publish fake Scene Activation CC value notification + event = Event( + type="value notification", + data={ + "source": "node", + "event": "value notification", + "nodeId": node.node_id, + "args": { + "commandClassName": "Scene Activation", + "commandClass": 43, + "endpoint": 0, + "property": "sceneId", + "propertyName": "sceneId", + "value": 1, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "min": 1, + "max": 255, + "label": "Scene ID", + }, + "ccVersion": 1, + }, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data[ + "some" + ] == "event.value_notification.scene_activation - device - zwave_js_value_notification - {}".format( + CommandClass.SCENE_ACTIVATION + ) + + +async def test_get_trigger_capabilities_scene_activation_value_notification( + hass, client, hank_binary_switch, integration +): + """Test we get the expected capabilities from a value_notification.scene_activation trigger.""" + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + capabilities = await device_trigger.async_get_trigger_capabilities( + hass, + { + "platform": "device", + "domain": DOMAIN, + "type": "event.value_notification.scene_activation", + "device_id": device.id, + "command_class": CommandClass.SCENE_ACTIVATION.value, + "property": "sceneId", + "property_key": None, + "endpoint": 0, + "subtype": "Endpoint 0", + }, + ) + 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": 1, + "valueMax": 255, + } + ] + + +async def test_failure_scenarios(hass, client, hank_binary_switch, integration): + """Test failure scenarios.""" + with pytest.raises(HomeAssistantError): + await async_attach_trigger( + hass, {"type": "failed.test", "device_id": "invalid_device_id"}, None, {} + ) + + with pytest.raises(HomeAssistantError): + await async_attach_trigger( + hass, + {"type": "event.failed_type", "device_id": "invalid_device_id"}, + None, + {}, + ) + + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + + with pytest.raises(HomeAssistantError): + await async_attach_trigger( + hass, {"type": "failed.test", "device_id": device.id}, None, {} + ) + + with pytest.raises(HomeAssistantError): + await async_attach_trigger( + hass, + {"type": "event.failed_type", "device_id": device.id}, + None, + {}, + ) + + with patch( + "homeassistant.components.zwave_js.device_trigger.async_get_node_from_device_id", + return_value=None, + ), patch( + "homeassistant.components.zwave_js.helpers.get_zwave_value_from_config", + return_value=None, + ): + assert ( + await async_get_trigger_capabilities( + hass, {"type": "failed.test", "device_id": "invalid_device_id"} + ) + == {} + ) + + with pytest.raises(HomeAssistantError): + async_get_node_status_sensor_entity_id(hass, "invalid_device_id") diff --git a/tests/fixtures/zwave_js/ge_in_wall_dimmer_switch_state.json b/tests/fixtures/zwave_js/ge_in_wall_dimmer_switch_state.json new file mode 100644 index 00000000000..58d3f0d06ec --- /dev/null +++ b/tests/fixtures/zwave_js/ge_in_wall_dimmer_switch_state.json @@ -0,0 +1,642 @@ +{ + "nodeId": 2, + "index": 0, + "installerIcon": 1536, + "userIcon": 1536, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": false, + "manufacturerId": 99, + "productId": 12344, + "productType": 18756, + "firmwareVersion": "5.26", + "zwavePlusVersion": 1, + "name": "LivingRoomLight", + "location": "LivingRoom", + "deviceConfig": { + "filename": "/opt/node_modules/@zwave-js/config/config/devices/0x0063/ge_14294_zw3005.json", + "manufacturer": "GE/Jasco", + "manufacturerId": 99, + "label": "14294 / ZW3005", + "description": "In-Wall Dimmer Switch", + "devices": [ + { + "productType": 18756, + "productId": 12344 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "associations": {}, + "paramInformation": { + "_map": {} + }, + "compat": { + "valueIdRegex": {}, + "treatBasicSetAsEvent": true + }, + "isEmbedded": true + }, + "label": "14294 / ZW3005", + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 2, + "index": 0, + "installerIcon": 1536, + "userIcon": 1536, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 1, + "label": "Multilevel Power Switch" + }, + "mandatorySupportedCCs": [32, 38, 39], + "mandatoryControlledCCs": [] + } + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 32, + "commandClassName": "Basic", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99 + } + }, + { + "endpoint": 0, + "commandClass": 32, + "commandClassName": "Basic", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "min": 0, + "max": 99 + } + }, + { + "endpoint": 0, + "commandClass": 32, + "commandClassName": "Basic", + "property": "event", + "propertyName": "event", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Event value", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "min": 0, + "max": 99 + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Transition duration" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99 + }, + "value": 35 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + } + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + } + } + }, + { + "endpoint": 0, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "min": 1, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Dimming duration" + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Night Light", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Defines the behavior of the blue LED. Default is on when switch is off.", + "label": "Night Light", + "default": 0, + "min": 0, + "max": 2, + "states": { + "0": "LED on when switch is OFF", + "1": "LED on when switch is ON", + "2": "LED always off" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Invert Switch", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Invert the ON/OFF Switch State.", + "label": "Invert Switch", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "No", + "1": "Yes" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyName": "Dim Rate Steps (Z-Wave Command)", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Number of steps or levels", + "label": "Dim Rate Steps (Z-Wave Command)", + "default": 1, + "min": 0, + "max": 99, + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyName": "Dim Rate Timing (Z-Wave)", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Timing of steps or levels", + "label": "Dim Rate Timing (Z-Wave)", + "default": 3, + "min": 1, + "max": 255, + "unit": "10ms", + "valueSize": 2, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 9, + "propertyName": "Dim Rate Steps (Manual)", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Number of steps or levels", + "label": "Dim Rate Steps (Manual)", + "default": 1, + "min": 1, + "max": 99, + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 10, + "propertyName": "Dim Rate Timing (Manual)", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Timing of steps", + "label": "Dim Rate Timing (Manual)", + "default": 3, + "min": 1, + "max": 255, + "unit": "10ms", + "valueSize": 2, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 11, + "propertyName": "Dim Rate Steps (All-On/All-Off)", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Number of steps or levels", + "label": "Dim Rate Steps (All-On/All-Off)", + "default": 1, + "min": 1, + "max": 99, + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 12, + "propertyName": "Dim Rate Timing (All-On/All-Off)", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Timing of steps or levels", + "label": "Dim Rate Timing (All-On/All-Off)", + "default": 3, + "min": 1, + "max": 255, + "unit": "10ms", + "valueSize": 2, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 99 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 18756 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 12344 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.34" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["5.26"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + } + ], + "isFrequentListening": false, + "maxDataRate": 40000, + "supportedDataRates": [40000], + "protocolVersion": 3, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 5, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 1, + "label": "Multilevel Power Switch" + }, + "mandatorySupportedCCs": [32, 38, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 32, + "name": "Basic", + "version": 1, + "isSecure": false + }, + { + "id": 38, + "name": "Multilevel Switch", + "version": 2, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 44, + "name": "Scene Actuator Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 86, + "name": "CRC-16 Encapsulation", + "version": 1, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 2, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + } + ], + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0063:0x4944:0x3038:5.26" +} diff --git a/tests/fixtures/zwave_js/lock_schlage_be469_state.json b/tests/fixtures/zwave_js/lock_schlage_be469_state.json index be1ddb9c3f0..f85a8e6b005 100644 --- a/tests/fixtures/zwave_js/lock_schlage_be469_state.json +++ b/tests/fixtures/zwave_js/lock_schlage_be469_state.json @@ -50,7 +50,68 @@ "index": 0 } ], - "commandClasses": [], + "commandClasses": [ + { + "id": 98, + "name": "Door Lock", + "version": 1, + "isSecure": true + }, + { + "id": 99, + "name": "User Code", + "version": 1, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 1, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 1, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": true + }, + { + "id": 133, + "name": "Association", + "version": 1, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ], "values": [ { "commandClassName": "Door Lock",