Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor zwave_js.sensor and add test coverage #93259

Merged
merged 5 commits into from
May 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 26 additions & 97 deletions homeassistant/components/zwave_js/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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)

Expand Down Expand Up @@ -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__(
Expand All @@ -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 (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
Expand All @@ -439,40 +429,25 @@ 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."""

@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
Expand All @@ -485,35 +460,19 @@ 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:
raman325 marked this conversation as resolved.
Show resolved Hide resolved
"""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
Expand All @@ -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."""
Expand All @@ -544,7 +493,7 @@ def extra_state_attributes(self) -> dict[str, str] | None:
return {ATTR_VALUE: value}


class ZWaveConfigParameterSensor(ZwaveSensorBase):
class ZWaveConfigParameterSensor(ZWaveListSensor):
"""Representation of a Z-Wave config parameter sensor."""

def __init__(
Expand Down Expand Up @@ -572,35 +521,15 @@ 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
):
return SensorDeviceClass.ENUM
return None

@property
def options(self) -> list[str] | None:
raman325 marked this conversation as resolved.
Show resolved Hide resolved
"""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
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
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."""
Expand Down
99 changes: 99 additions & 0 deletions tests/components/zwave_js/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from zwave_js_server.model.node import Node

from homeassistant.components.sensor import (
ATTR_OPTIONS,
ATTR_STATE_CLASS,
SensorDeviceClass,
SensorStateClass,
Expand All @@ -27,6 +28,7 @@
ATTR_UNIT_OF_MEASUREMENT,
PERCENTAGE,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
EntityCategory,
UnitOfElectricCurrent,
UnitOfElectricPotential,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down