Refactor zwave_js.sensor and add test coverage (#93259)

* Refactor zwave_js.sensor and add test coverage

* use walrus

* inherit config parameter class from list class

* use walrus in more places

* improve config parameter test
This commit is contained in:
Raman Gupta 2023-05-22 11:58:10 -04:00 committed by GitHub
parent d6997d8656
commit e1dd7118e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 125 additions and 97 deletions

View file

@ -38,10 +38,10 @@ from homeassistant.const import (
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 @@ async def async_setup_entry(
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 @@ async def async_setup_entry(
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 @@ async def async_setup_entry(
)
class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity):
class ZwaveSensor(ZWaveBaseEntity, SensorEntity):
"""Basic Representation of a Z-Wave sensor."""
def __init__(
@ -403,26 +394,25 @@ class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity):
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 (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)
class ZWaveNumericSensor(ZwaveSensorBase):
class ZWaveNumericSensor(ZwaveSensor):
"""Representation of a Z-Wave Numeric sensor."""
@callback
@ -439,18 +429,6 @@ class ZWaveNumericSensor(ZwaveSensorBase):
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):
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
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,35 +460,19 @@ class ZWaveMeterSensor(ZWaveNumericSensor):
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:
"""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
@ -525,16 +484,6 @@ class ZWaveListSensor(ZwaveSensorBase):
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 @@ class ZWaveListSensor(ZwaveSensorBase):
return {ATTR_VALUE: value}
class ZWaveConfigParameterSensor(ZwaveSensorBase):
class ZWaveConfigParameterSensor(ZWaveListSensor):
"""Representation of a Z-Wave config parameter sensor."""
def __init__(
@ -572,8 +521,8 @@ class ZWaveConfigParameterSensor(ZwaveSensorBase):
@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
@ -581,26 +530,6 @@ class ZWaveConfigParameterSensor(ZwaveSensorBase):
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."""

View file

@ -7,6 +7,7 @@ from zwave_js_server.event import Event
from zwave_js_server.model.node import Node
from homeassistant.components.sensor import (
ATTR_OPTIONS,
ATTR_STATE_CLASS,
SensorDeviceClass,
SensorStateClass,
@ -27,6 +28,7 @@ from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
PERCENTAGE,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
EntityCategory,
UnitOfElectricCurrent,
UnitOfElectricPotential,
@ -103,6 +105,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 +191,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 +239,53 @@ 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
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