From 65187c6f11222bffacf358396de09d6dde9566d6 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 30 May 2023 11:49:55 -0400 Subject: [PATCH] Add zwave config parameter entities (#92223) * Add zwave config parameter entities * Remove unused entity const * remove unusued imports * review comments * switch to reserved values * fix test --- .../components/zwave_js/binary_sensor.py | 23 ++++ homeassistant/components/zwave_js/const.py | 3 + .../components/zwave_js/discovery.py | 120 +++++++++++++----- homeassistant/components/zwave_js/entity.py | 19 +++ homeassistant/components/zwave_js/number.py | 42 +++++- homeassistant/components/zwave_js/select.py | 23 ++++ homeassistant/components/zwave_js/sensor.py | 26 ++-- homeassistant/components/zwave_js/switch.py | 38 +++++- tests/components/zwave_js/common.py | 3 - .../components/zwave_js/test_binary_sensor.py | 26 ++++ tests/components/zwave_js/test_init.py | 4 +- tests/components/zwave_js/test_number.py | 36 +++++- tests/components/zwave_js/test_select.py | 26 ++++ tests/components/zwave_js/test_sensor.py | 71 ++++------- tests/components/zwave_js/test_switch.py | 77 ++++++++++- 15 files changed, 431 insertions(+), 106 deletions(-) diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index 0051d6ccbf2..ef5cdd1b1d2 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -315,6 +315,10 @@ async def async_setup_entry( config_entry, driver, info, property_description ) ) + elif info.platform_hint == "config_parameter": + entities.append( + ZWaveConfigParameterBinarySensor(config_entry, driver, info) + ) else: # boolean sensor entities.append(ZWaveBooleanBinarySensor(config_entry, driver, info)) @@ -411,3 +415,22 @@ class ZWavePropertyBinarySensor(ZWaveBaseEntity, BinarySensorEntity): if self.info.primary_value.value is None: return None return self.info.primary_value.value in self.entity_description.on_states + + +class ZWaveConfigParameterBinarySensor(ZWaveBooleanBinarySensor): + """Representation of a Z-Wave config parameter binary sensor.""" + + _attr_entity_category = EntityCategory.DIAGNOSTIC + + def __init__( + self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize a ZWaveConfigParameterBinarySensor entity.""" + super().__init__(config_entry, driver, info) + + property_key_name = self.info.primary_value.property_key_name + # Entity class attributes + self._attr_name = self.generate_name( + alternate_value_name=self.info.primary_value.property_name, + additional_info=[property_key_name] if property_key_name else None, + ) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 27d4dc00368..6a85780ce78 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -36,6 +36,9 @@ EVENT_DEVICE_ADDED_TO_REGISTRY = f"{DOMAIN}_device_added_to_registry" LOGGER = logging.getLogger(__package__) +# constants extra state attributes +ATTR_RESERVED_VALUES = "reserved_values" # ConfigurationValue number entities + # constants for events ZWAVE_JS_VALUE_NOTIFICATION_EVENT = f"{DOMAIN}_value_notification" ZWAVE_JS_NOTIFICATION_EVENT = f"{DOMAIN}_notification" diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 2bd7210b043..c17cbace657 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Generator from dataclasses import asdict, dataclass, field -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast from awesomeversion import AwesomeVersion from zwave_js_server.const import ( @@ -41,7 +41,11 @@ from zwave_js_server.const.command_class.thermostat import ( from zwave_js_server.exceptions import UnknownValueData from zwave_js_server.model.device_class import DeviceClassItem from zwave_js_server.model.node import Node as ZwaveNode -from zwave_js_server.model.value import Value as ZwaveValue +from zwave_js_server.model.value import ( + ConfigurationValue, + ConfigurationValueType, + Value as ZwaveValue, +) from homeassistant.backports.enum import StrEnum from homeassistant.const import EntityCategory, Platform @@ -205,32 +209,6 @@ class ZWaveDiscoverySchema: entity_category: EntityCategory | None = None -def get_config_parameter_discovery_schema( - property_: set[str | int] | None = None, - property_name: set[str] | None = None, - property_key: set[str | int | None] | None = None, - **kwargs: Any, -) -> ZWaveDiscoverySchema: - """Return a discovery schema for a config parameter. - - Supports all keyword arguments to ZWaveValueDiscoverySchema except platform, hint, - and primary_value. - """ - return ZWaveDiscoverySchema( - platform=Platform.SENSOR, - hint="config_parameter", - primary_value=ZWaveValueDiscoverySchema( - command_class={CommandClass.CONFIGURATION}, - property=property_, - property_name=property_name, - property_key=property_key, - type={ValueType.NUMBER}, - ), - entity_registry_enabled_default=False, - **kwargs, - ) - - DOOR_LOCK_CURRENT_MODE_SCHEMA = ZWaveValueDiscoverySchema( command_class={CommandClass.DOOR_LOCK}, property={CURRENT_MODE_PROPERTY}, @@ -596,13 +574,6 @@ DISCOVERY_SCHEMAS = [ ), absent_values=[SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA], ), - # ====== START OF CONFIG PARAMETER SPECIFIC MAPPING SCHEMAS ======= - # Door lock mode config parameter. Functionality equivalent to Notification CC - # list sensors. - get_config_parameter_discovery_schema( - property_name={"Door lock mode"}, - device_class_generic={"Entry Control"}, - ), # ====== START OF GENERIC MAPPING SCHEMAS ======= # locks # Door Lock CC @@ -1112,6 +1083,85 @@ def async_discover_single_value( # by other schemas/platforms return + if value.command_class == CommandClass.CONFIGURATION: + yield from async_discover_single_configuration_value( + cast(ConfigurationValue, value) + ) + + +@callback +def async_discover_single_configuration_value( + value: ConfigurationValue, +) -> Generator[ZwaveDiscoveryInfo, None, None]: + """Run discovery on a single ZWave configuration value and return matching schema info.""" + if value.metadata.writeable and value.metadata.readable: + if value.configuration_value_type == ConfigurationValueType.ENUMERATED: + yield ZwaveDiscoveryInfo( + node=value.node, + primary_value=value, + assumed_state=False, + platform=Platform.SELECT, + platform_hint="config_parameter", + platform_data=None, + additional_value_ids_to_watch=set(), + entity_registry_enabled_default=False, + ) + elif value.configuration_value_type in ( + ConfigurationValueType.RANGE, + ConfigurationValueType.MANUAL_ENTRY, + ): + if value.metadata.type == ValueType.BOOLEAN or ( + value.metadata.min == 0 and value.metadata.max == 1 + ): + yield ZwaveDiscoveryInfo( + node=value.node, + primary_value=value, + assumed_state=False, + platform=Platform.SWITCH, + platform_hint="config_parameter", + platform_data=None, + additional_value_ids_to_watch=set(), + entity_registry_enabled_default=False, + ) + else: + yield ZwaveDiscoveryInfo( + node=value.node, + primary_value=value, + assumed_state=False, + platform=Platform.NUMBER, + platform_hint="config_parameter", + platform_data=None, + additional_value_ids_to_watch=set(), + entity_registry_enabled_default=False, + ) + elif not value.metadata.writeable and value.metadata.readable: + if value.metadata.type == ValueType.BOOLEAN or ( + value.metadata.min == 0 + and value.metadata.max == 1 + and not value.metadata.states + ): + yield ZwaveDiscoveryInfo( + node=value.node, + primary_value=value, + assumed_state=False, + platform=Platform.BINARY_SENSOR, + platform_hint="config_parameter", + platform_data=None, + additional_value_ids_to_watch=set(), + entity_registry_enabled_default=False, + ) + else: + yield ZwaveDiscoveryInfo( + node=value.node, + primary_value=value, + assumed_state=False, + platform=Platform.SENSOR, + platform_hint="config_parameter", + platform_data=None, + additional_value_ids_to_watch=set(), + entity_registry_enabled_default=False, + ) + @callback def check_value(value: ZwaveValue, schema: ZWaveValueDiscoverySchema) -> bool: diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index de6adda121a..ba2d753112c 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -2,13 +2,16 @@ from __future__ import annotations from collections.abc import Sequence +from typing import Any from zwave_js_server.const import NodeStatus +from zwave_js_server.exceptions import BaseZwaveJSServerError from zwave_js_server.model.driver import Driver from zwave_js_server.model.value import Value as ZwaveValue, get_value_id_str from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity @@ -292,3 +295,19 @@ class ZWaveBaseEntity(Entity): ): self.watched_value_ids.add(return_value.value_id) return return_value + + async def _async_set_value( + self, + value: ZwaveValue, + new_value: Any, + options: dict | None = None, + wait_for_result: bool | None = None, + ) -> bool | None: + """Set value on node.""" + try: + return await self.info.node.async_set_value( + value, new_value, options=options, wait_for_result=wait_for_result + ) + except BaseZwaveJSServerError as err: + LOGGER.error("Unable to set value %s: %s", value.value_id, err) + raise HomeAssistantError from err diff --git a/homeassistant/components/zwave_js/number.py b/homeassistant/components/zwave_js/number.py index 52302c64b33..4cffa7a8522 100644 --- a/homeassistant/components/zwave_js/number.py +++ b/homeassistant/components/zwave_js/number.py @@ -1,7 +1,8 @@ """Support for Z-Wave controls using the number platform.""" from __future__ import annotations -from typing import cast +from collections.abc import Mapping +from typing import Any, cast from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import TARGET_VALUE_PROPERTY @@ -10,12 +11,13 @@ from zwave_js_server.model.value import Value from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DATA_CLIENT, DOMAIN +from .const import ATTR_RESERVED_VALUES, DATA_CLIENT, DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity @@ -38,6 +40,10 @@ async def async_setup_entry( entities: list[ZWaveBaseEntity] = [] if info.platform_hint == "volume": entities.append(ZwaveVolumeNumberEntity(config_entry, driver, info)) + elif info.platform_hint == "config_parameter": + entities.append( + ZWaveConfigParameterNumberEntity(config_entry, driver, info) + ) else: entities.append(ZwaveNumberEntity(config_entry, driver, info)) async_add_entities(entities) @@ -98,7 +104,37 @@ class ZwaveNumberEntity(ZWaveBaseEntity, NumberEntity): """Set new value.""" if (target_value := self._target_value) is None: raise HomeAssistantError("Missing target value on device.") - await self.info.node.async_set_value(target_value, value) + await self._async_set_value(target_value, value) + + +class ZWaveConfigParameterNumberEntity(ZwaveNumberEntity): + """Representation of a Z-Wave config parameter number.""" + + _attr_entity_category = EntityCategory.CONFIG + + def __init__( + self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize a ZWaveConfigParameterNumber entity.""" + super().__init__(config_entry, driver, info) + + property_key_name = self.info.primary_value.property_key_name + # Entity class attributes + self._attr_name = self.generate_name( + alternate_value_name=self.info.primary_value.property_name, + additional_info=[property_key_name] if property_key_name else None, + ) + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return extra state attributes for entity.""" + if not self.info.primary_value.metadata.states: + return None + return { + ATTR_RESERVED_VALUES: { + int(k): v for k, v in self.info.primary_value.metadata.states.items() + } + } class ZwaveVolumeNumberEntity(ZWaveBaseEntity, NumberEntity): diff --git a/homeassistant/components/zwave_js/select.py b/homeassistant/components/zwave_js/select.py index f74c98117b4..49d4fec4a20 100644 --- a/homeassistant/components/zwave_js/select.py +++ b/homeassistant/components/zwave_js/select.py @@ -42,6 +42,10 @@ async def async_setup_entry( entities.append( ZwaveMultilevelSwitchSelectEntity(config_entry, driver, info) ) + elif info.platform_hint == "config_parameter": + entities.append( + ZWaveConfigParameterSelectEntity(config_entry, driver, info) + ) else: entities.append(ZwaveSelectEntity(config_entry, driver, info)) async_add_entities(entities) @@ -91,6 +95,25 @@ class ZwaveSelectEntity(ZWaveBaseEntity, SelectEntity): await self.info.node.async_set_value(self.info.primary_value, int(key)) +class ZWaveConfigParameterSelectEntity(ZwaveSelectEntity): + """Representation of a Z-Wave config parameter select.""" + + _attr_entity_category = EntityCategory.CONFIG + + def __init__( + self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize a ZWaveConfigParameterSelect entity.""" + super().__init__(config_entry, driver, info) + + property_key_name = self.info.primary_value.property_key_name + # Entity class attributes + self._attr_name = self.generate_name( + alternate_value_name=self.info.primary_value.property_name, + additional_info=[property_key_name] if property_key_name else None, + ) + + class ZwaveDefaultToneSelectEntity(ZWaveBaseEntity, SelectEntity): """Representation of a Z-Wave default tone select entity.""" diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 469f66b792b..1a292cb17ef 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -6,14 +6,14 @@ from typing import cast import voluptuous as vol from zwave_js_server.client import Client as ZwaveClient -from zwave_js_server.const import CommandClass, ConfigurationValueType, NodeStatus +from zwave_js_server.const import CommandClass, NodeStatus from zwave_js_server.const.command_class.meter import ( RESET_METER_OPTION_TARGET_VALUE, RESET_METER_OPTION_TYPE, ) from zwave_js_server.model.driver import Driver from zwave_js_server.model.node import Node as ZwaveNode -from zwave_js_server.model.value import ConfigurationValue +from zwave_js_server.model.value import ConfigurationValue, ConfigurationValueType from zwave_js_server.util.command_class.meter import get_meter_type from homeassistant.components.sensor import ( @@ -490,6 +490,13 @@ class ZWaveListSensor(ZwaveSensor): additional_info=[self.info.primary_value.property_key_name], ) + @property + def options(self) -> list[str] | None: + """Return options for enum sensor.""" + if self.device_class == SensorDeviceClass.ENUM: + return list(self.info.primary_value.metadata.states.values()) + return None + @property def device_class(self) -> SensorDeviceClass | None: """Return sensor device class.""" @@ -499,13 +506,6 @@ class ZWaveListSensor(ZwaveSensor): return SensorDeviceClass.ENUM return None - @property - def options(self) -> list[str] | None: - """Return options for enum sensor.""" - if self.device_class == SensorDeviceClass.ENUM: - return list(self.info.primary_value.metadata.states.values()) - return None - @property def extra_state_attributes(self) -> dict[str, str] | None: """Return the device specific state attributes.""" @@ -518,6 +518,8 @@ class ZWaveListSensor(ZwaveSensor): class ZWaveConfigParameterSensor(ZWaveListSensor): """Representation of a Z-Wave config parameter sensor.""" + _attr_entity_category = EntityCategory.DIAGNOSTIC + def __init__( self, config_entry: ConfigEntry, @@ -537,7 +539,6 @@ class ZWaveConfigParameterSensor(ZWaveListSensor): self._attr_name = self.generate_name( alternate_value_name=self.info.primary_value.property_name, additional_info=[property_key_name] if property_key_name else None, - name_prefix="Config parameter", ) @property @@ -555,10 +556,7 @@ class ZWaveConfigParameterSensor(ZWaveListSensor): @property def extra_state_attributes(self) -> dict[str, str] | None: """Return the device specific state attributes.""" - if ( - self._primary_value.configuration_value_type == ConfigurationValueType.RANGE - or (value := self.info.primary_value.value) is None - ): + if (value := self.info.primary_value.value) is None: return None # add the value's int value as property for multi-value (list) items return {ATTR_VALUE: value} diff --git a/homeassistant/components/zwave_js/switch.py b/homeassistant/components/zwave_js/switch.py index ce640f92140..409bcd1dbb7 100644 --- a/homeassistant/components/zwave_js/switch.py +++ b/homeassistant/components/zwave_js/switch.py @@ -12,6 +12,7 @@ from zwave_js_server.model.driver import Driver from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -41,6 +42,8 @@ async def async_setup_entry( entities.append( ZWaveBarrierEventSignalingSwitch(config_entry, driver, info) ) + elif info.platform_hint == "config_parameter": + entities.append(ZWaveConfigParameterSwitch(config_entry, driver, info)) elif info.platform_hint == "indicator": entities.append(ZWaveIndicatorSwitch(config_entry, driver, info)) else: @@ -79,12 +82,12 @@ class ZWaveSwitch(ZWaveBaseEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" if self._target_value is not None: - await self.info.node.async_set_value(self._target_value, True) + await self._async_set_value(self._target_value, True) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" if self._target_value is not None: - await self.info.node.async_set_value(self._target_value, False) + await self._async_set_value(self._target_value, False) class ZWaveIndicatorSwitch(ZWaveSwitch): @@ -129,7 +132,7 @@ class ZWaveBarrierEventSignalingSwitch(ZWaveBaseEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" - await self.info.node.async_set_value( + await self._async_set_value( self.info.primary_value, BarrierEventSignalingSubsystemState.ON ) # this value is not refreshed, so assume success @@ -138,7 +141,7 @@ class ZWaveBarrierEventSignalingSwitch(ZWaveBaseEntity, SwitchEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" - await self.info.node.async_set_value( + await self._async_set_value( self.info.primary_value, BarrierEventSignalingSubsystemState.OFF ) # this value is not refreshed, so assume success @@ -152,3 +155,30 @@ class ZWaveBarrierEventSignalingSwitch(ZWaveBaseEntity, SwitchEntity): self._state = ( self.info.primary_value.value == BarrierEventSignalingSubsystemState.ON ) + + +class ZWaveConfigParameterSwitch(ZWaveSwitch): + """Representation of a Z-Wave config parameter switch.""" + + _attr_entity_category = EntityCategory.CONFIG + + def __init__( + self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize a ZWaveConfigParameterSwitch entity.""" + super().__init__(config_entry, driver, info) + + property_key_name = self.info.primary_value.property_key_name + # Entity class attributes + self._attr_name = self.generate_name( + alternate_value_name=self.info.primary_value.property_name, + additional_info=[property_key_name] if property_key_name else None, + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + await self._async_set_value(self.info.primary_value, 1) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + await self._async_set_value(self.info.primary_value, 0) diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index aee2322b2af..3da63419a4b 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -39,9 +39,6 @@ BULB_6_MULTI_COLOR_LIGHT_ENTITY = "light.bulb_6_multi_color" EATON_RF9640_ENTITY = "light.allloaddimmer" AEON_SMART_SWITCH_LIGHT_ENTITY = "light.smart_switch_6" SCHLAGE_BE469_LOCK_ENTITY = "lock.touchscreen_deadbolt" -ID_LOCK_CONFIG_PARAMETER_SENSOR = ( - "sensor.z_wave_module_for_id_lock_150_and_101_config_parameter_door_lock_mode" -) ZEN_31_ENTITY = "light.kitchen_under_cabinet_lights" METER_ENERGY_SENSOR = "sensor.smart_switch_6_electric_consumed_kwh" METER_VOLTAGE_SENSOR = "sensor.smart_switch_6_electric_consumed_v" diff --git a/tests/components/zwave_js/test_binary_sensor.py b/tests/components/zwave_js/test_binary_sensor.py index ca7d20aa9cf..a3ae9954d2f 100644 --- a/tests/components/zwave_js/test_binary_sensor.py +++ b/tests/components/zwave_js/test_binary_sensor.py @@ -259,3 +259,29 @@ async def test_property_sensor_door_status( state = hass.states.get(PROPERTY_DOOR_STATUS_BINARY_SENSOR) assert state assert state.state == STATE_UNKNOWN + + +async def test_config_parameter_binary_sensor( + hass: HomeAssistant, climate_adc_t3000, integration +) -> None: + """Test config parameter binary sensor is created.""" + binary_sensor_entity_id = "binary_sensor.adc_t3000_system_configuration_override" + ent_reg = er.async_get(hass) + entity_entry = ent_reg.async_get(binary_sensor_entity_id) + assert entity_entry + assert entity_entry.disabled + assert entity_entry.entity_category == EntityCategory.DIAGNOSTIC + + updated_entry = ent_reg.async_update_entity( + binary_sensor_entity_id, **{"disabled_by": None} + ) + assert updated_entry != entity_entry + assert updated_entry.disabled is False + + # reload integration and check if entity is correctly there + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(binary_sensor_entity_id) + assert state + assert state.state == STATE_OFF diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 45cf23a3f1e..2b1e16a6c12 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -963,7 +963,7 @@ async def test_removed_device( # Check how many entities there are ent_reg = er.async_get(hass) entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id) - assert len(entity_entries) == 36 + assert len(entity_entries) == 62 # Remove a node and reload the entry old_node = driver.controller.nodes.pop(13) @@ -975,7 +975,7 @@ async def test_removed_device( device_entries = dr.async_entries_for_config_entry(dev_reg, integration.entry_id) assert len(device_entries) == 2 entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id) - assert len(entity_entries) == 23 + assert len(entity_entries) == 38 assert dev_reg.async_get_device({get_device_id(driver, old_node)}) is None diff --git a/tests/components/zwave_js/test_number.py b/tests/components/zwave_js/test_number.py index d94af41c9d6..7229d10ebad 100644 --- a/tests/components/zwave_js/test_number.py +++ b/tests/components/zwave_js/test_number.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest from zwave_js_server.event import Event -from homeassistant.const import STATE_UNKNOWN +from homeassistant.const import STATE_UNKNOWN, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er @@ -229,3 +229,37 @@ async def test_disabled_basic_number( assert entity_entry assert entity_entry.disabled assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + +async def test_config_parameter_number( + hass: HomeAssistant, climate_adc_t3000, integration +) -> None: + """Test config parameter number is created.""" + number_entity_id = "number.adc_t3000_heat_staging_delay" + number_with_states_entity_id = "number.adc_t3000_calibration_temperature" + ent_reg = er.async_get(hass) + for entity_id in (number_entity_id, number_with_states_entity_id): + entity_entry = ent_reg.async_get(entity_id) + assert entity_entry + assert entity_entry.disabled + assert entity_entry.entity_category == EntityCategory.CONFIG + + for entity_id in (number_entity_id, number_with_states_entity_id): + updated_entry = ent_reg.async_update_entity(entity_id, **{"disabled_by": None}) + assert updated_entry != entity_entry + assert updated_entry.disabled is False + + # reload integration and check if entity is correctly there + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(number_entity_id) + assert state + assert state.state == "30.0" + assert "reserved_values" not in state.attributes + + state = hass.states.get(number_with_states_entity_id) + assert state + assert state.state == "0.0" + assert "reserved_values" in state.attributes + assert state.attributes["reserved_values"] == {-1: "Disabled"} diff --git a/tests/components/zwave_js/test_select.py b/tests/components/zwave_js/test_select.py index dc367e637fc..c63f0c429fd 100644 --- a/tests/components/zwave_js/test_select.py +++ b/tests/components/zwave_js/test_select.py @@ -294,3 +294,29 @@ async def test_multilevel_switch_select_no_value( assert state assert state.state == STATE_UNKNOWN + + +async def test_config_parameter_select( + hass: HomeAssistant, climate_adc_t3000, integration +) -> None: + """Test config parameter select is created.""" + select_entity_id = "select.adc_t3000_hvac_system_type" + ent_reg = er.async_get(hass) + entity_entry = ent_reg.async_get(select_entity_id) + assert entity_entry + assert entity_entry.disabled + assert entity_entry.entity_category == EntityCategory.CONFIG + + updated_entry = ent_reg.async_update_entity( + select_entity_id, **{"disabled_by": None} + ) + assert updated_entry != entity_entry + assert updated_entry.disabled is False + + # reload integration and check if entity is correctly there + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(select_entity_id) + assert state + assert state.state == "Normal" diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index 6056d639181..783dd14f234 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -45,7 +45,6 @@ from .common import ( CURRENT_SENSOR, ENERGY_SENSOR, HUMIDITY_SENSOR, - ID_LOCK_CONFIG_PARAMETER_SENSOR, METER_ENERGY_SENSOR, NOTIFICATION_MOTION_SENSOR, POWER_SENSOR, @@ -188,7 +187,9 @@ async def test_disabled_notification_sensor( state = hass.states.get(NOTIFICATION_MOTION_SENSOR) assert state.state == "Motion detection" - assert state.attributes["value"] == 8 + assert state.attributes[ATTR_VALUE] == 8 + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENUM + assert state.attributes[ATTR_OPTIONS] == ["idle", "Motion detection"] event = Event( "value updated", @@ -218,13 +219,34 @@ async def test_disabled_notification_sensor( async def test_config_parameter_sensor( - hass: HomeAssistant, lock_id_lock_as_id150, integration + hass: HomeAssistant, climate_adc_t3000, lock_id_lock_as_id150, integration ) -> None: """Test config parameter sensor is created.""" + sensor_entity_id = "sensor.adc_t3000_system_configuration_cool_stages" + sensor_with_states_entity_id = "sensor.adc_t3000_power_source" ent_reg = er.async_get(hass) - entity_entry = ent_reg.async_get(ID_LOCK_CONFIG_PARAMETER_SENSOR) - assert entity_entry - assert entity_entry.disabled + for entity_id in (sensor_entity_id, sensor_with_states_entity_id): + entity_entry = ent_reg.async_get(entity_id) + assert entity_entry + assert entity_entry.disabled + assert entity_entry.entity_category == EntityCategory.DIAGNOSTIC + + for entity_id in (sensor_entity_id, sensor_with_states_entity_id): + updated_entry = ent_reg.async_update_entity(entity_id, **{"disabled_by": None}) + assert updated_entry != entity_entry + assert updated_entry.disabled is False + + # reload integration and check if entity is correctly there + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(sensor_entity_id) + assert state + assert state.state == "1" + + state = hass.states.get(sensor_with_states_entity_id) + assert state + assert state.state == "C-Wire" updated_entry = ent_reg.async_update_entity( entity_entry.entity_id, **{"disabled_by": None} @@ -236,43 +258,6 @@ async def test_config_parameter_sensor( await hass.config_entries.async_reload(integration.entry_id) await hass.async_block_till_done() - state = hass.states.get(ID_LOCK_CONFIG_PARAMETER_SENSOR) - assert state - assert state.state == "Disable Away Manual Lock" - assert state.attributes[ATTR_VALUE] == 0 - assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENUM - assert state.attributes[ATTR_OPTIONS] == [ - "Disable Away Manual Lock", - "Disable Away Auto Lock", - "Enable Away Manual Lock", - "Enable Away Auto Lock", - ] - - event = Event( - "value updated", - { - "source": "node", - "event": "value updated", - "nodeId": lock_id_lock_as_id150.node_id, - "args": { - "commandClassName": "Configuration", - "commandClass": 112, - "endpoint": 0, - "property": 1, - "newValue": None, - "prevValue": 0, - "propertyName": "Door lock mode", - }, - }, - ) - - lock_id_lock_as_id150.receive_event(event) - await hass.async_block_till_done() - state = hass.states.get(ID_LOCK_CONFIG_PARAMETER_SENSOR) - assert state - assert state.state == STATE_UNKNOWN - assert ATTR_VALUE not in state.attributes - async def test_node_status_sensor( hass: HomeAssistant, client, lock_id_lock_as_id150, integration diff --git a/tests/components/zwave_js/test_switch.py b/tests/components/zwave_js/test_switch.py index 87ad7838ff1..ebf7d9f441f 100644 --- a/tests/components/zwave_js/test_switch.py +++ b/tests/components/zwave_js/test_switch.py @@ -1,12 +1,16 @@ """Test the Z-Wave JS switch platform.""" +import pytest from zwave_js_server.const import CURRENT_VALUE_PROPERTY, CommandClass from zwave_js_server.event import Event +from zwave_js_server.exceptions import FailedZWaveCommand from zwave_js_server.model.node import Node from homeassistant.components.switch import DOMAIN, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.components.zwave_js.helpers import ZwaveValueMatcher -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN, EntityCategory from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er from .common import SWITCH_ENTITY, replace_value_of_zwave_value @@ -209,3 +213,74 @@ async def test_switch_no_value( assert state assert state.state == STATE_UNKNOWN + + +async def test_config_parameter_switch( + hass: HomeAssistant, hank_binary_switch, integration, client +) -> None: + """Test config parameter switch is created.""" + switch_entity_id = "switch.smart_plug_with_two_usb_ports_overload_protection" + ent_reg = er.async_get(hass) + entity_entry = ent_reg.async_get(switch_entity_id) + assert entity_entry + assert entity_entry.disabled + + updated_entry = ent_reg.async_update_entity( + switch_entity_id, **{"disabled_by": None} + ) + assert updated_entry != entity_entry + assert updated_entry.disabled is False + assert entity_entry.entity_category == EntityCategory.CONFIG + + # reload integration and check if entity is correctly there + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(switch_entity_id) + assert state + assert state.state == STATE_ON + + client.async_send_command.reset_mock() + + # Test turning on + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {"entity_id": switch_entity_id}, 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"] == hank_binary_switch.node_id + assert args["value"] == 1 + assert args["valueId"] == { + "commandClass": 112, + "endpoint": 0, + "property": 20, + } + + client.async_send_command.reset_mock() + + # Test turning off + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {"entity_id": switch_entity_id}, 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"] == hank_binary_switch.node_id + assert args["value"] == 0 + assert args["valueId"] == { + "commandClass": 112, + "endpoint": 0, + "property": 20, + } + + client.async_send_command.reset_mock() + client.async_send_command.side_effect = FailedZWaveCommand("test", 1, "test") + + # Test turning off error raises proper exception + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {"entity_id": switch_entity_id}, blocking=True + )