diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index c91b6b40437..648b0109e3c 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -283,7 +283,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): return UnitOfTemperature.CELSIUS @property - def hvac_mode(self) -> HVACMode: + def hvac_mode(self) -> HVACMode | None: """Return hvac operation ie. heat, cool mode.""" if self._current_mode is None: # Thermostat(valve) with no support for setting @@ -292,7 +292,10 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): if self._current_mode.value is None: # guard missing value return HVACMode.HEAT - return ZW_HVAC_MODE_MAP.get(int(self._current_mode.value), HVACMode.HEAT_COOL) + mode = ZW_HVAC_MODE_MAP.get(int(self._current_mode.value)) + if mode is not None and mode not in self._hvac_modes: + return None + return mode @property def hvac_modes(self) -> list[HVACMode]: @@ -548,12 +551,17 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): """Set new target preset mode.""" assert self._current_mode is not None if preset_mode == PRESET_NONE: - # try to restore to the (translated) main hvac mode - await self.async_set_hvac_mode(self.hvac_mode) + # Try to restore to the (translated) main hvac mode. + if (hvac_mode := self.hvac_mode) is None: + # Current preset mode doesn't map to a supported HVAC mode. + # Pick the first supported non-off mode. + hvac_mode = next( + mode for mode in self._hvac_modes if mode != HVACMode.OFF + ) + await self.async_set_hvac_mode(hvac_mode) return - preset_mode_value = self._hvac_presets.get(preset_mode) - if preset_mode_value is None: - raise ValueError(f"Received an invalid preset mode: {preset_mode}") + + preset_mode_value = self._hvac_presets[preset_mode] await self._async_set_value(self._current_mode, preset_mode_value) diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index acea2ed8a21..9f981efd120 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -176,6 +176,12 @@ def climate_eurotronic_spirit_z_state_fixture() -> dict[str, Any]: return load_json_object_fixture("climate_eurotronic_spirit_z_state.json", DOMAIN) +@pytest.fixture(name="climate_eurotronic_comet_z_state", scope="package") +def climate_eurotronic_comet_z_state_fixture() -> dict[str, Any]: + """Load the climate Eurotronic Comet Z thermostat node state fixture data.""" + return load_json_object_fixture("climate_eurotronic_comet_z_state.json", DOMAIN) + + @pytest.fixture(name="climate_heatit_z_trm6_state", scope="package") def climate_heatit_z_trm6_state_fixture() -> dict[str, Any]: """Load the climate HEATIT Z-TRM6 thermostat node state fixture data.""" @@ -851,6 +857,16 @@ def climate_eurotronic_spirit_z_fixture( return node +@pytest.fixture(name="climate_eurotronic_comet_z") +def climate_eurotronic_comet_z_fixture( + client: MagicMock, climate_eurotronic_comet_z_state: dict[str, Any] +) -> Node: + """Mock a climate Eurotronic Comet Z node.""" + node = Node(client, copy.deepcopy(climate_eurotronic_comet_z_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="climate_heatit_z_trm6") def climate_heatit_z_trm6_fixture(client, climate_heatit_z_trm6_state) -> Node: """Mock a climate radio HEATIT Z-TRM6 node.""" diff --git a/tests/components/zwave_js/fixtures/climate_eurotronic_comet_z_state.json b/tests/components/zwave_js/fixtures/climate_eurotronic_comet_z_state.json new file mode 100644 index 00000000000..7152e1cfe16 --- /dev/null +++ b/tests/components/zwave_js/fixtures/climate_eurotronic_comet_z_state.json @@ -0,0 +1,528 @@ +{ + "nodeId": 2, + "index": 0, + "installerIcon": 4608, + "userIcon": 4608, + "status": 4, + "ready": true, + "isListening": false, + "isRouting": true, + "isSecure": true, + "manufacturerId": 328, + "productId": 3, + "productType": 4, + "firmwareVersion": "14.1.4", + "zwavePlusVersion": 2, + "deviceConfig": { + "filename": "/data/db/devices/0x0148/cometz_700.json", + "isEmbedded": true, + "manufacturer": "Eurotronics", + "manufacturerId": 328, + "label": "Comet Z", + "description": "Radiator Thermostat", + "devices": [ + { + "productType": 4, + "productId": 3 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "preferred": false, + "associations": {}, + "paramInformation": { + "_map": {} + } + }, + "label": "Comet Z", + "interviewAttempts": 0, + "isFrequentListening": "1000ms", + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 7, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 8, + "label": "Thermostat" + }, + "specific": { + "key": 6, + "label": "General Thermostat V2" + } + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0148:0x0004:0x0003:14.1.4", + "statistics": { + "commandsTX": 23, + "commandsRX": 21, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 3, + "rtt": 877.4, + "rssi": -79, + "lwr": { + "protocolDataRate": 2, + "repeaters": [], + "rssi": -78, + "repeaterRSSI": [] + } + }, + "highestSecurityClass": 0, + "isControllerNode": false, + "keepAwake": false, + "protocol": 0, + "sdkVersion": "7.15.4", + "endpoints": [ + { + "nodeId": 2, + "index": 0, + "installerIcon": 4608, + "userIcon": 4608, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 8, + "label": "Thermostat" + }, + "specific": { + "key": 6, + "label": "General Thermostat V2" + } + }, + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 11, + "isSecure": true + }, + { + "id": 38, + "name": "Multilevel Switch", + "version": 1, + "isSecure": true + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 3, + "isSecure": true + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 3, + "isSecure": true + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": true + } + ] + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "restorePrevious", + "propertyName": "restorePrevious", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Restore previous value", + "states": { + "true": "Restore" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 11, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + }, + "unit": "\u00b0C", + "stateful": true, + "secret": false + }, + "value": 18.5 + }, + { + "endpoint": 0, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "mode", + "propertyName": "mode", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Thermostat mode", + "min": 0, + "max": 255, + "states": { + "0": "Off", + "1": "Heat", + "11": "Energy heat", + "15": "Full power", + "31": "Manufacturer specific" + }, + "stateful": true, + "secret": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "manufacturerData", + "propertyName": "manufacturerData", + "ccVersion": 3, + "metadata": { + "type": "buffer", + "readable": true, + "writeable": false, + "label": "Manufacturer data", + "stateful": true, + "secret": false + }, + "value": { + "type": "Buffer", + "data": [] + } + }, + { + "endpoint": 0, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyKey": 1, + "propertyName": "setpoint", + "propertyKeyName": "Heating", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Setpoint (Heating)", + "ccSpecific": { + "setpointType": 1 + }, + "min": 8, + "max": 28, + "unit": "\u00b0C", + "stateful": true, + "secret": false + }, + "value": 21 + }, + { + "endpoint": 0, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyKey": 11, + "propertyName": "setpoint", + "propertyKeyName": "Energy Save Heating", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Setpoint (Energy Save Heating)", + "ccSpecific": { + "setpointType": 11 + }, + "min": 8, + "max": 28, + "unit": "\u00b0C", + "stateful": true, + "secret": false + }, + "value": 16 + }, + { + "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, + "stateful": true, + "secret": false + }, + "value": 328 + }, + { + "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, + "stateful": true, + "secret": false + }, + "value": 4 + }, + { + "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, + "stateful": true, + "secret": false + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery level", + "min": 0, + "max": 100, + "unit": "%", + "stateful": true, + "secret": false + }, + "value": 45 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version", + "stateful": true, + "secret": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 3, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions", + "stateful": true, + "secret": false + }, + "value": ["14.1", "1.6"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + }, + "value": "7.15" + } + ] +} diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index a356613aa7a..59d1c09d61d 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -1,6 +1,7 @@ """Test the Z-Wave JS climate platform.""" import copy +from unittest.mock import MagicMock import pytest from zwave_js_server.const import CommandClass @@ -56,6 +57,8 @@ from .common import ( replace_value_of_zwave_value, ) +from tests.common import MockConfigEntry + @pytest.fixture def platforms() -> list[str]: @@ -1001,3 +1004,329 @@ async def test_thermostat_unknown_values( state = hass.states.get(CLIMATE_RADIO_THERMOSTAT_ENTITY) assert ATTR_HVAC_ACTION not in state.attributes + + +async def test_set_preset_mode_manufacturer_specific( + hass: HomeAssistant, + client: MagicMock, + climate_eurotronic_comet_z: Node, + integration: MockConfigEntry, +) -> None: + """Test setting preset mode to manufacturer specific. + + This tests the Eurotronic Comet Z thermostat which has a + "Manufacturer specific" thermostat mode (value 31) that is + exposed as a preset mode. + """ + node = climate_eurotronic_comet_z + entity_id = "climate.radiator_thermostat" + + state = hass.states.get(entity_id) + assert state + assert state.state == HVACMode.HEAT + assert state.attributes[ATTR_TEMPERATURE] == 21 + assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + + # Test setting preset mode to "Manufacturer specific" + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_PRESET_MODE: "Manufacturer specific", + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 2 + assert args["valueId"] == { + "commandClass": 64, + "endpoint": 0, + "property": "mode", + } + assert args["value"] == 31 + + client.async_send_command.reset_mock() + + # Simulate the device updating to manufacturer specific mode + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 2, + "args": { + "commandClassName": "Thermostat Mode", + "commandClass": 64, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": 31, + "prevValue": 1, + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(entity_id) + assert state + # Mode 31 is not in ZW_HVAC_MODE_MAP, so hvac_mode is unknown. + assert state.state == "unknown" + assert state.attributes[ATTR_PRESET_MODE] == "Manufacturer specific" + + # Test restoring hvac mode by setting preset to none. + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_PRESET_MODE: PRESET_NONE, + }, + blocking=True, + ) + + assert client.async_send_command.call_count == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 2 + assert args["valueId"] == { + "commandClass": 64, + "endpoint": 0, + "property": "mode", + } + assert args["value"] == 1 + + client.async_send_command.reset_mock() + + +async def test_preset_mode_mapped_to_unsupported_hvac_mode( + hass: HomeAssistant, + client: MagicMock, + climate_eurotronic_comet_z: Node, + integration: MockConfigEntry, +) -> None: + """Test preset mapping to an HVAC mode the entity doesn't support. + + The Away mode (13) maps to HVACMode.HEAT_COOL in ZW_HVAC_MODE_MAP, + but the Comet Z only supports OFF and HEAT. The hvac_mode property + should return None for this unsupported mapping. + """ + node = climate_eurotronic_comet_z + entity_id = "climate.radiator_thermostat" + + # Simulate the device being set to Away mode (13). + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 2, + "args": { + "commandClassName": "Thermostat Mode", + "commandClass": 64, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": 13, + "prevValue": 1, + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(entity_id) + assert state + # Away maps to HEAT_COOL which the device doesn't support, + # so hvac_mode returns None. + assert state.state == "unknown" + + +async def test_set_preset_mode_mapped_preset( + hass: HomeAssistant, + client: MagicMock, + climate_eurotronic_comet_z: Node, + integration: MockConfigEntry, +) -> None: + """Test that a preset mapping to a supported HVAC mode shows that mode. + + The Eurotronic Comet Z has "Energy heat" (mode 11 = HEATING_ECON) which + maps to HVACMode.HEAT in ZW_HVAC_MODE_MAP. Since the device supports + heat, hvac_mode should return heat while in this preset. + """ + node = climate_eurotronic_comet_z + entity_id = "climate.radiator_thermostat" + + state = hass.states.get(entity_id) + assert state + assert state.state == HVACMode.HEAT + + # Set preset mode to "Energy heat" + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_PRESET_MODE: "Energy heat", + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["value"] == 11 + + client.async_send_command.reset_mock() + + # Simulate the device updating to energy heat mode + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 2, + "args": { + "commandClassName": "Thermostat Mode", + "commandClass": 64, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": 11, + "prevValue": 1, + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(entity_id) + assert state + # Energy heat (HEATING_ECON) maps to HVACMode.HEAT which the device + # supports, so hvac_mode returns heat. + assert state.state == HVACMode.HEAT + assert state.attributes[ATTR_PRESET_MODE] == "Energy heat" + + # Clear preset - should restore to heat (the mapped mode). + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_PRESET_MODE: PRESET_NONE, + }, + blocking=True, + ) + + assert client.async_send_command.call_count == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["value"] == 1 + + client.async_send_command.reset_mock() + + +async def test_set_preset_mode_none_while_in_hvac_mode( + hass: HomeAssistant, + client: MagicMock, + climate_eurotronic_comet_z: Node, + integration: MockConfigEntry, +) -> None: + """Test setting preset mode to none while already in an HVAC mode.""" + entity_id = "climate.radiator_thermostat" + + state = hass.states.get(entity_id) + assert state + assert state.state == HVACMode.HEAT + assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + + # Setting preset to none while already in an HVAC mode restores + # the current hvac mode. + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_PRESET_MODE: PRESET_NONE, + }, + blocking=True, + ) + + assert client.async_send_command.call_count == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 2 + assert args["valueId"] == { + "commandClass": 64, + "endpoint": 0, + "property": "mode", + } + assert args["value"] == 1 + + +async def test_set_preset_mode_none_unmapped_preset( + hass: HomeAssistant, + client: MagicMock, + climate_eurotronic_comet_z: Node, + integration: MockConfigEntry, +) -> None: + """Test clearing an unmapped preset falls back to first supported HVAC mode. + + When the device is in a preset mode that has no mapping in ZW_HVAC_MODE_MAP + (e.g. "Manufacturer specific"), hvac_mode returns None. Setting preset to + none should fall back to the first supported non-off HVAC mode. + """ + node = climate_eurotronic_comet_z + entity_id = "climate.radiator_thermostat" + + # Simulate the device being externally changed to "Manufacturer specific" + # mode without HA having set a preset first. + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 2, + "args": { + "commandClassName": "Thermostat Mode", + "commandClass": 64, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": 31, + "prevValue": 1, + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(entity_id) + assert state + assert state.state == "unknown" + assert state.attributes[ATTR_PRESET_MODE] == "Manufacturer specific" + + client.async_send_command.reset_mock() + + # Setting preset to none should default to heat since there is no + # stored previous HVAC mode. + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_PRESET_MODE: PRESET_NONE, + }, + blocking=True, + ) + + assert client.async_send_command.call_count == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 2 + assert args["valueId"] == { + "commandClass": 64, + "endpoint": 0, + "property": "mode", + } + assert args["value"] == 1