From 092d77982bc3d2e0a61ba535e55755e4d828cc40 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 19 May 2023 00:09:18 -0400 Subject: [PATCH 1/5] Refactor zwave_js.sensor and add test coverage --- homeassistant/components/zwave_js/sensor.py | 115 ++++---------------- tests/components/zwave_js/test_sensor.py | 91 ++++++++++++++++ 2 files changed, 113 insertions(+), 93 deletions(-) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index f7fe5f52d7917d..8d6eab6e0a12f1 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -38,10 +38,10 @@ UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from .const import ( ATTR_METER_TYPE, @@ -311,11 +311,7 @@ def async_add_sensor(info: ZwaveDiscoveryInfo) -> None: entity_description = get_entity_description(data) - if info.platform_hint == "string_sensor": - entities.append( - ZWaveStringSensor(config_entry, driver, info, entity_description) - ) - elif info.platform_hint == "numeric_sensor": + if info.platform_hint == "numeric_sensor": entities.append( ZWaveNumericSensor( config_entry, @@ -340,12 +336,7 @@ def async_add_sensor(info: ZwaveDiscoveryInfo) -> None: ZWaveMeterSensor(config_entry, driver, info, entity_description) ) else: - LOGGER.warning( - "Sensor not implemented for %s/%s", - info.platform_hint, - info.primary_value.property_name, - ) - return + entities.append(ZwaveSensor(config_entry, driver, info, entity_description)) async_add_entities(entities) @@ -383,7 +374,7 @@ def async_add_node_status_sensor(node: ZwaveNode) -> None: ) -class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity): +class ZwaveSensor(ZWaveBaseEntity, SensorEntity): """Basic Representation of a Z-Wave sensor.""" def __init__( @@ -403,26 +394,25 @@ def __init__( self._attr_force_update = True self._attr_name = self.generate_name(include_value_name=True) - -class ZWaveStringSensor(ZwaveSensorBase): - """Representation of a Z-Wave String sensor.""" - @property - def native_value(self) -> str | None: + def native_value(self) -> StateType: """Return state of the sensor.""" - if self.info.primary_value.value is None: - return None - return str(self.info.primary_value.value) + key = str(self.info.primary_value.value) + if key not in self.info.primary_value.metadata.states: + return self.info.primary_value.value + return str(self.info.primary_value.metadata.states[key]) @property def native_unit_of_measurement(self) -> str | None: """Return unit of measurement the value is expressed in.""" + if super().native_unit_of_measurement is not None: + return super().native_unit_of_measurement if self.info.primary_value.metadata.unit is None: return None return str(self.info.primary_value.metadata.unit) -class ZWaveNumericSensor(ZwaveSensorBase): +class ZWaveNumericSensor(ZwaveSensor): """Representation of a Z-Wave Numeric sensor.""" @callback @@ -439,18 +429,6 @@ def native_value(self) -> float: return 0 return round(float(self.info.primary_value.value), 2) - @property - def native_unit_of_measurement(self) -> str | None: - """Return unit of measurement the value is expressed in.""" - if self.entity_description.native_unit_of_measurement is not None: - return self.entity_description.native_unit_of_measurement - if self._attr_native_unit_of_measurement is not None: - return self._attr_native_unit_of_measurement - if self.info.primary_value.metadata.unit is None: - return None - - return str(self.info.primary_value.metadata.unit) - class ZWaveMeterSensor(ZWaveNumericSensor): """Representation of a Z-Wave Meter CC sensor.""" @@ -458,21 +436,18 @@ class ZWaveMeterSensor(ZWaveNumericSensor): @property def extra_state_attributes(self) -> Mapping[str, int | str] | None: """Return extra state attributes.""" - if meter_type := get_meter_type(self.info.primary_value): - return { - ATTR_METER_TYPE: meter_type.value, - ATTR_METER_TYPE_NAME: meter_type.name, - } - return None + meter_type = get_meter_type(self.info.primary_value) + return { + ATTR_METER_TYPE: meter_type.value, + ATTR_METER_TYPE_NAME: meter_type.name, + } async def async_reset_meter( self, meter_type: int | None = None, value: int | None = None ) -> None: """Reset meter(s) on device.""" node = self.info.node - primary_value = self.info.primary_value - if (endpoint := primary_value.endpoint) is None: - raise HomeAssistantError("Missing endpoint on device.") + endpoint = self.info.primary_value.endpoint or 0 options = {} if meter_type is not None: options[RESET_METER_OPTION_TYPE] = meter_type @@ -485,29 +460,13 @@ async def async_reset_meter( LOGGER.debug( "Meters on node %s endpoint %s reset with the following options: %s", node, - primary_value.endpoint, + endpoint, options, ) -class ZWaveListSensor(ZwaveSensorBase): - """Representation of a Z-Wave List sensor with multiple states.""" - - def __init__( - self, - config_entry: ConfigEntry, - driver: Driver, - info: ZwaveDiscoveryInfo, - entity_description: SensorEntityDescription, - unit_of_measurement: str | None = None, - ) -> None: - """Initialize a ZWaveListSensor entity.""" - super().__init__( - config_entry, driver, info, entity_description, unit_of_measurement - ) - - # Entity class attributes - self._attr_name = self.generate_name(include_value_name=True) +class ZWaveListSensor(ZwaveSensor): + """Representation of a Z-Wave Numeric sensor with multiple states.""" @property def device_class(self) -> SensorDeviceClass | None: @@ -525,16 +484,6 @@ def options(self) -> list[str] | None: return list(self.info.primary_value.metadata.states.values()) return None - @property - def native_value(self) -> str | None: - """Return state of the sensor.""" - if self.info.primary_value.value is None: - return None - key = str(self.info.primary_value.value) - if key not in self.info.primary_value.metadata.states: - return key - return str(self.info.primary_value.metadata.states[key]) - @property def extra_state_attributes(self) -> dict[str, str] | None: """Return the device specific state attributes.""" @@ -544,7 +493,7 @@ def extra_state_attributes(self) -> dict[str, str] | None: return {ATTR_VALUE: value} -class ZWaveConfigParameterSensor(ZwaveSensorBase): +class ZWaveConfigParameterSensor(ZwaveSensor): """Representation of a Z-Wave config parameter sensor.""" def __init__( @@ -581,26 +530,6 @@ def device_class(self) -> SensorDeviceClass | None: 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 native_value(self) -> str | None: - """Return state of the sensor.""" - if self.info.primary_value.value is None: - return None - key = str(self.info.primary_value.value) - if ( - self._primary_value.configuration_value_type == ConfigurationValueType.RANGE - or (key not in self.info.primary_value.metadata.states) - ): - return key - return str(self.info.primary_value.metadata.states[key]) - @property def extra_state_attributes(self) -> dict[str, str] | None: """Return the device specific state attributes.""" diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index e41158a5d1d396..91d9f956969284 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -27,6 +27,7 @@ ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, STATE_UNAVAILABLE, + STATE_UNKNOWN, EntityCategory, UnitOfElectricCurrent, UnitOfElectricPotential, @@ -103,6 +104,30 @@ async def test_numeric_sensor( assert ATTR_DEVICE_CLASS not in state.attributes assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + event = Event( + "value updated", + { + "source": "node", + "event": "value updated", + "nodeId": express_controls_ezmultipli.node_id, + "args": { + "commandClassName": "Multilevel Sensor", + "commandClass": 49, + "endpoint": 0, + "property": "Illuminance", + "propertyName": "Illuminance", + "newValue": None, + "prevValue": 61, + }, + }, + ) + + express_controls_ezmultipli.receive_event(event) + await hass.async_block_till_done() + state = hass.states.get("sensor.hsm200_illuminance") + assert state + assert state.state == "0" + async def test_energy_sensors( hass: HomeAssistant, hank_binary_switch, integration @@ -165,6 +190,32 @@ async def test_disabled_notification_sensor( assert state.state == "Motion detection" assert state.attributes["value"] == 8 + event = Event( + "value updated", + { + "source": "node", + "event": "value updated", + "nodeId": multisensor_6.node_id, + "args": { + "commandClassName": "Notification", + "commandClass": 113, + "endpoint": 0, + "property": "Home Security", + "propertyKey": "Motion sensor status", + "newValue": None, + "prevValue": 0, + "propertyName": "Home Security", + "propertyKeyName": "Motion sensor status", + }, + }, + ) + + multisensor_6.receive_event(event) + await hass.async_block_till_done() + state = hass.states.get(NOTIFICATION_MOTION_SENSOR) + assert state + assert state.state == STATE_UNKNOWN + async def test_disabled_indcator_sensor( hass: HomeAssistant, climate_radio_thermostat_ct100_plus, integration @@ -187,6 +238,46 @@ async def test_config_parameter_sensor( assert entity_entry assert entity_entry.disabled + updated_entry = ent_reg.async_update_entity( + entity_entry.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(ID_LOCK_CONFIG_PARAMETER_SENSOR) + assert state + assert state.state == "Disable Away Manual Lock" + assert state.attributes[ATTR_VALUE] == 0 + + 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 From 251b7eed052856185befae4166ea47fbd387fcd0 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 22 May 2023 11:01:39 -0400 Subject: [PATCH 2/5] use walrus --- homeassistant/components/zwave_js/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 8d6eab6e0a12f1..d6898a0533b66c 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -405,8 +405,8 @@ def native_value(self) -> StateType: @property def native_unit_of_measurement(self) -> str | None: """Return unit of measurement the value is expressed in.""" - if super().native_unit_of_measurement is not None: - return super().native_unit_of_measurement + if (unit := super().native_unit_of_measurement) is not None: + return unit if self.info.primary_value.metadata.unit is None: return None return str(self.info.primary_value.metadata.unit) From 1e17cb2a236b90e4e88de715d014f958ebb67870 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 22 May 2023 11:05:16 -0400 Subject: [PATCH 3/5] inherit config parameter class from list class --- homeassistant/components/zwave_js/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index d6898a0533b66c..60bf0b5ca07706 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -493,7 +493,7 @@ def extra_state_attributes(self) -> dict[str, str] | None: return {ATTR_VALUE: value} -class ZWaveConfigParameterSensor(ZwaveSensor): +class ZWaveConfigParameterSensor(ZWaveListSensor): """Representation of a Z-Wave config parameter sensor.""" def __init__( From 19865c0d1db4fe5090f5255ac38cadcc8a78ff2a Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 22 May 2023 11:21:15 -0400 Subject: [PATCH 4/5] use walrus in more places --- homeassistant/components/zwave_js/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 60bf0b5ca07706..313110169c3a4e 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -471,8 +471,8 @@ class ZWaveListSensor(ZwaveSensor): @property def device_class(self) -> SensorDeviceClass | None: """Return sensor device class.""" - if super().device_class is not None: - return super().device_class + if (device_class := super().device_class) is not None: + return device_class if self.info.primary_value.metadata.states: return SensorDeviceClass.ENUM return None @@ -521,8 +521,8 @@ def __init__( @property def device_class(self) -> SensorDeviceClass | None: """Return sensor device class.""" - if super().device_class is not None: - return super().device_class + if (device_class := super(ZwaveSensor, self).device_class) is not None: + return device_class if ( self._primary_value.configuration_value_type == ConfigurationValueType.ENUMERATED From 7a32ffd2d6a0b4e00581015752b87aa9601b335c Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 22 May 2023 11:27:08 -0400 Subject: [PATCH 5/5] improve config parameter test --- tests/components/zwave_js/test_sensor.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index 91d9f956969284..d38466539e92ba 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -7,6 +7,7 @@ from zwave_js_server.model.node import Node from homeassistant.components.sensor import ( + ATTR_OPTIONS, ATTR_STATE_CLASS, SensorDeviceClass, SensorStateClass, @@ -252,6 +253,13 @@ async def test_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",