diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 7c77105bbea..8e980e19765 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -13,12 +13,13 @@ from zwave_js_server.model.notification import ( EntryControlNotification, NotificationNotification, ) -from zwave_js_server.model.value import ValueNotification +from zwave_js_server.model.value import Value, ValueNotification from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_ID, ATTR_DOMAIN, + ATTR_ENTITY_ID, CONF_URL, EVENT_HOMEASSISTANT_STOP, ) @@ -63,9 +64,10 @@ from .const import ( LOGGER, ZWAVE_JS_NOTIFICATION_EVENT, ZWAVE_JS_VALUE_NOTIFICATION_EVENT, + ZWAVE_JS_VALUE_UPDATED_EVENT, ) -from .discovery import async_discover_values -from .helpers import async_enable_statistics, get_device_id +from .discovery import ZwaveDiscoveryInfo, async_discover_values +from .helpers import async_enable_statistics, get_device_id, get_unique_id from .migrate import async_migrate_discovered_value from .services import ZWaveServices @@ -140,6 +142,8 @@ async def async_setup_entry( # noqa: C901 if device.id not in registered_unique_ids: registered_unique_ids[device.id] = defaultdict(set) + value_updates_disc_info = [] + # run discovery on all node values and create/update entities for disc_info in async_discover_values(node): platform = disc_info.platform @@ -168,6 +172,21 @@ async def async_setup_entry( # noqa: C901 hass, f"{DOMAIN}_{entry.entry_id}_add_{platform}", disc_info ) + # Capture discovery info for values we want to watch for updates + if disc_info.assumed_state: + value_updates_disc_info.append(disc_info) + + # add listener for value updated events if necessary + if value_updates_disc_info: + unsubscribe_callbacks.append( + node.on( + "value updated", + lambda event: async_on_value_updated( + value_updates_disc_info, event["value"] + ), + ) + ) + # add listener for stateless node value notification events unsubscribe_callbacks.append( node.on( @@ -274,6 +293,52 @@ async def async_setup_entry( # noqa: C901 hass.bus.async_fire(ZWAVE_JS_NOTIFICATION_EVENT, event_data) + @callback + def async_on_value_updated( + value_updates_disc_info: list[ZwaveDiscoveryInfo], value: Value + ) -> None: + """Fire value updated event.""" + # Get the discovery info for the value that was updated. If we can't + # find the discovery info, we don't need to fire an event + try: + disc_info = next( + disc_info + for disc_info in value_updates_disc_info + if disc_info.primary_value.value_id == value.value_id + ) + except StopIteration: + return + + device = dev_reg.async_get_device({get_device_id(client, value.node)}) + + unique_id = get_unique_id( + client.driver.controller.home_id, disc_info.primary_value.value_id + ) + entity_id = ent_reg.async_get_entity_id(disc_info.platform, DOMAIN, unique_id) + + raw_value = value_ = value.value + if value.metadata.states: + value_ = value.metadata.states.get(str(value), value_) + + hass.bus.async_fire( + ZWAVE_JS_VALUE_UPDATED_EVENT, + { + ATTR_NODE_ID: value.node.node_id, + ATTR_HOME_ID: client.driver.controller.home_id, + ATTR_DEVICE_ID: device.id, # type: ignore + ATTR_ENTITY_ID: entity_id, + ATTR_COMMAND_CLASS: value.command_class, + ATTR_COMMAND_CLASS_NAME: value.command_class_name, + ATTR_ENDPOINT: value.endpoint, + ATTR_PROPERTY: value.property_, + ATTR_PROPERTY_NAME: value.property_name, + ATTR_PROPERTY_KEY: value.property_key, + ATTR_PROPERTY_KEY_NAME: value.property_key_name, + ATTR_VALUE: value_, + ATTR_VALUE_RAW: raw_value, + }, + ) + # connect and throw error if connection failed try: async with timeout(CONNECT_TIMEOUT): diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 54c1ca78e30..629cd222bd4 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -21,6 +21,7 @@ LOGGER = logging.getLogger(__package__) # constants for events ZWAVE_JS_VALUE_NOTIFICATION_EVENT = f"{DOMAIN}_value_notification" ZWAVE_JS_NOTIFICATION_EVENT = f"{DOMAIN}_notification" +ZWAVE_JS_VALUE_UPDATED_EVENT = f"{DOMAIN}_value_updated" ATTR_NODE_ID = "node_id" ATTR_HOME_ID = "home_id" ATTR_ENDPOINT = "endpoint" diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index a7df9998f6a..21e75a8ea51 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -21,6 +21,8 @@ class ZwaveDiscoveryInfo: node: ZwaveNode # the value object itself for primary value primary_value: ZwaveValue + # bool to specify whether state is assumed and events should be fired on value update + assumed_state: bool # the home assistant platform for which an entity should be created platform: str # hint for the platform about this discovered entity @@ -87,6 +89,8 @@ class ZWaveDiscoverySchema: absent_values: list[ZWaveValueDiscoverySchema] | None = None # [optional] bool to specify if this primary value may be discovered by multiple platforms allow_multi: bool = False + # [optional] bool to specify whether state is assumed and events should be fired on value update + assumed_state: bool = False def get_config_parameter_discovery_schema( @@ -123,6 +127,10 @@ SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA = ZWaveValueDiscoverySchema( type={"number"}, ) +SWITCH_BINARY_CURRENT_VALUE_SCHEMA = ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_BINARY}, property={"currentValue"} +) + # For device class mapping see: # https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/deviceClasses.json DISCOVERY_SCHEMAS = [ @@ -197,6 +205,15 @@ DISCOVERY_SCHEMAS = [ product_type={0x0003}, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, ), + # Vision Security ZL7432 In Wall Dual Relay Switch + ZWaveDiscoverySchema( + platform="switch", + manufacturer_id={0x0109}, + product_id={0x1711, 0x1717}, + product_type={0x2017}, + primary_value=SWITCH_BINARY_CURRENT_VALUE_SCHEMA, + assumed_state=True, + ), # ====== START OF CONFIG PARAMETER SPECIFIC MAPPING SCHEMAS ======= # Door lock mode config parameter. Functionality equivalent to Notification CC # list sensors. @@ -365,9 +382,7 @@ DISCOVERY_SCHEMAS = [ # binary switches ZWaveDiscoverySchema( platform="switch", - primary_value=ZWaveValueDiscoverySchema( - command_class={CommandClass.SWITCH_BINARY}, property={"currentValue"} - ), + primary_value=SWITCH_BINARY_CURRENT_VALUE_SCHEMA, ), # binary switch # barrier operator signaling states @@ -513,6 +528,7 @@ def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None yield ZwaveDiscoveryInfo( node=value.node, primary_value=value, + assumed_state=schema.assumed_state, platform=schema.platform, platform_hint=schema.hint, ) diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 458cc721650..20efa9ed7db 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -224,3 +224,8 @@ class ZWaveBaseEntity(Entity): def should_poll(self) -> bool: """No polling needed.""" return False + + @property + def assumed_state(self) -> bool: + """Return True if unable to access real state of the entity.""" + return self.info.assumed_state diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 0135351fd0d..4d656522eef 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -337,6 +337,12 @@ def climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_stat ) +@pytest.fixture(name="vision_security_zl7432_state", scope="session") +def vision_security_zl7432_state_fixture(): + """Load the vision security zl7432 switch node state fixture data.""" + return json.loads(load_fixture("zwave_js/vision_security_zl7432_state.json")) + + @pytest.fixture(name="client") def mock_client_fixture(controller_state, version_state): """Mock a client.""" @@ -637,3 +643,11 @@ def climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_fixt ) client.driver.controller.nodes[node.node_id] = node return node + + +@pytest.fixture(name="vision_security_zl7432") +def vision_security_zl7432_fixture(client, vision_security_zl7432_state): + """Mock a vision security zl7432 node.""" + node = Node(client, copy.deepcopy(vision_security_zl7432_state)) + client.driver.controller.nodes[node.node_id] = node + return node diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py index 810ccb8df33..cbc9d756292 100644 --- a/tests/components/zwave_js/test_discovery.py +++ b/tests/components/zwave_js/test_discovery.py @@ -35,3 +35,16 @@ async def test_inovelli_lzw36(hass, client, inovelli_lzw36, integration): state = hass.states.get("fan.family_room_combo_2") assert state + + +async def test_vision_security_zl7432( + hass, client, vision_security_zl7432, integration +): + """Test Vision Security ZL7432 is caught by the device specific discovery.""" + for entity_id in ( + "switch.in_wall_dual_relay_switch", + "switch.in_wall_dual_relay_switch_2", + ): + state = hass.states.get(entity_id) + assert state + assert state.attributes["assumed_state"] diff --git a/tests/components/zwave_js/test_events.py b/tests/components/zwave_js/test_events.py index 15de6aa8887..d66cd52be40 100644 --- a/tests/components/zwave_js/test_events.py +++ b/tests/components/zwave_js/test_events.py @@ -194,3 +194,68 @@ async def test_notifications(hass, hank_binary_switch, integration, client): assert events[1].data["event_data"] == "555" assert events[1].data["command_class"] == CommandClass.ENTRY_CONTROL assert events[1].data["command_class_name"] == "Entry Control" + + +async def test_value_updated(hass, vision_security_zl7432, integration, client): + """Test value updated events.""" + node = vision_security_zl7432 + events = async_capture_events(hass, "zwave_js_value_updated") + + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 7, + "args": { + "commandClassName": "Switch Binary", + "commandClass": 37, + "endpoint": 1, + "property": "currentValue", + "newValue": 1, + "prevValue": 0, + "propertyName": "currentValue", + }, + }, + ) + + node.receive_event(event) + # wait for the event + await hass.async_block_till_done() + assert len(events) == 1 + assert events[0].data["home_id"] == client.driver.controller.home_id + assert events[0].data["node_id"] == 7 + assert events[0].data["entity_id"] == "switch.in_wall_dual_relay_switch" + assert events[0].data["command_class"] == CommandClass.SWITCH_BINARY + assert events[0].data["command_class_name"] == "Switch Binary" + assert events[0].data["endpoint"] == 1 + assert events[0].data["property_name"] == "currentValue" + assert events[0].data["property"] == "currentValue" + assert events[0].data["value"] == 1 + assert events[0].data["value_raw"] == 1 + + # Try a value updated event on a value we aren't watching to make sure + # no event fires + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 7, + "args": { + "commandClassName": "Basic", + "commandClass": 32, + "endpoint": 1, + "property": "currentValue", + "newValue": 1, + "prevValue": 0, + "propertyName": "currentValue", + }, + }, + ) + + node.receive_event(event) + # wait for the event + await hass.async_block_till_done() + # We should only still have captured one event + assert len(events) == 1 diff --git a/tests/fixtures/zwave_js/vision_security_zl7432_state.json b/tests/fixtures/zwave_js/vision_security_zl7432_state.json new file mode 100644 index 00000000000..d37e82ea3af --- /dev/null +++ b/tests/fixtures/zwave_js/vision_security_zl7432_state.json @@ -0,0 +1,433 @@ +{ + "nodeId": 7, + "index": 0, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": false, + "manufacturerId": 265, + "productId": 5911, + "productType": 8215, + "firmwareVersion": "13.7", + "deviceConfig": { + "filename": "/opt/node_modules/@zwave-js/config/config/devices/0x0109/zl7432.json", + "manufacturer": "Vision Security", + "manufacturerId": 265, + "label": "ZL7432", + "description": "In Wall Dual Relay Switch", + "devices": [ + { + "productType": 8215, + "productId": 5905 + }, + { + "productType": 8215, + "productId": 5911 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "associations": {} + }, + "label": "ZL7432", + "neighbors": [ + 1, + 10, + 11, + 12, + 13, + 14, + 15, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 26, + 27, + 28, + 29, + 3, + 32, + 33, + 34, + 4, + 47, + 48, + 5, + 50, + 51, + 52, + 53, + 56, + 58, + 59, + 69, + 8, + 9 + ], + "endpointCountIsDynamic": false, + "endpointsHaveIdenticalCapabilities": true, + "individualEndpointCount": 2, + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 7, + "index": 0, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 7, + "index": 1, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 7, + "index": 2, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + } + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 96, + "commandClassName": "Multi Channel", + "property": "endpointIndizes", + "propertyName": "endpointIndizes", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": [1, 2] + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 265 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 8215 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 5911 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 6 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "3.67" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["13.7"] + }, + { + "endpoint": 1, + "commandClass": 32, + "commandClassName": "Basic", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99 + } + }, + { + "endpoint": 1, + "commandClass": 32, + "commandClassName": "Basic", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "min": 0, + "max": 99 + } + }, + { + "endpoint": 1, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value" + }, + "value": false + }, + { + "endpoint": 1, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value" + }, + "value": false + }, + { + "endpoint": 2, + "commandClass": 32, + "commandClassName": "Basic", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99 + } + }, + { + "endpoint": 2, + "commandClass": 32, + "commandClassName": "Basic", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "min": 0, + "max": 99 + } + }, + { + "endpoint": 2, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value" + }, + "value": false + }, + { + "endpoint": 2, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value" + }, + "value": false + } + ], + "interviewStage": 6, + "isFrequentListening": false, + "maxDataRate": 40000, + "supportedDataRates": [40000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 3, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + } + ] +}