From 383c63000ecbb81525a3537bf4fe3176341dbfb1 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 4 Oct 2023 22:55:18 -0400 Subject: [PATCH] Handle invalid scale for zwave_js multilevel/meter sensors (#101173) * Handle invalid scale for zwave_js multilevel/meter sensors * Remove logging statement --- .../zwave_js/discovery_data_template.py | 40 ++++++---- tests/components/zwave_js/test_sensor.py | 80 +++++++++++++++++-- 2 files changed, 95 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index 7a274df41f2..b633e2a614f 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Iterable, Mapping from dataclasses import dataclass, field import logging -from typing import Any, cast +from typing import Any, TypeVar, cast from zwave_js_server.const import CommandClass from zwave_js_server.const.command_class.energy_production import ( @@ -87,6 +87,7 @@ from zwave_js_server.const.command_class.multilevel_sensor import ( MultilevelSensorScaleType, MultilevelSensorType, ) +from zwave_js_server.exceptions import UnknownValueData from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.value import ( ConfigurationValue as ZwaveConfigurationValue, @@ -355,24 +356,22 @@ class NumericSensorDataTemplateData: unit_of_measurement: str | None = None +T = TypeVar( + "T", + MultilevelSensorType, + MultilevelSensorScaleType, + MeterScaleType, + EnergyProductionParameter, + EnergyProductionScaleType, +) + + class NumericSensorDataTemplate(BaseDiscoverySchemaDataTemplate): """Data template class for Z-Wave Sensor entities.""" @staticmethod def find_key_from_matching_set( - enum_value: MultilevelSensorType - | MultilevelSensorScaleType - | MeterScaleType - | EnergyProductionParameter - | EnergyProductionScaleType, - set_map: Mapping[ - str, - list[MultilevelSensorType] - | list[MultilevelSensorScaleType] - | list[MeterScaleType] - | list[EnergyProductionScaleType] - | list[EnergyProductionParameter], - ], + enum_value: T, set_map: Mapping[str, list[T]] ) -> str | None: """Find a key in a set map that matches a given enum value.""" for key, value_set in set_map.items(): @@ -393,7 +392,11 @@ class NumericSensorDataTemplate(BaseDiscoverySchemaDataTemplate): return NumericSensorDataTemplateData(ENTITY_DESC_KEY_BATTERY, PERCENTAGE) if value.command_class == CommandClass.METER: - meter_scale_type = get_meter_scale_type(value) + try: + meter_scale_type = get_meter_scale_type(value) + except UnknownValueData: + return NumericSensorDataTemplateData() + unit = self.find_key_from_matching_set(meter_scale_type, METER_UNIT_MAP) # We do this because even though these are energy scales, they don't meet # the unit requirements for the energy device class. @@ -418,8 +421,11 @@ class NumericSensorDataTemplate(BaseDiscoverySchemaDataTemplate): ) if value.command_class == CommandClass.SENSOR_MULTILEVEL: - sensor_type = get_multilevel_sensor_type(value) - multilevel_sensor_scale_type = get_multilevel_sensor_scale_type(value) + try: + sensor_type = get_multilevel_sensor_type(value) + multilevel_sensor_scale_type = get_multilevel_sensor_scale_type(value) + except UnknownValueData: + return NumericSensorDataTemplateData() unit = self.find_key_from_matching_set( multilevel_sensor_scale_type, MULTILEVEL_SENSOR_UNIT_MAP ) diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index d452f28b3bf..f00413b0d80 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -130,6 +130,40 @@ async def test_numeric_sensor( assert state.state == "0" +async def test_invalid_multilevel_sensor_scale( + hass: HomeAssistant, client, multisensor_6_state, integration +) -> None: + """Test a multilevel sensor with an invalid scale.""" + node_state = copy.deepcopy(multisensor_6_state) + value = next( + value + for value in node_state["values"] + if value["commandClass"] == 49 and value["property"] == "Air temperature" + ) + value["metadata"]["ccSpecific"]["scale"] = -1 + value["metadata"]["unit"] = None + + event = Event( + "node added", + { + "source": "controller", + "event": "node added", + "node": node_state, + "result": "", + }, + ) + client.driver.controller.receive_event(event) + await hass.async_block_till_done() + + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert state + assert state.state == "9.0" + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_DEVICE_CLASS not in state.attributes + assert ATTR_STATE_CLASS not in state.attributes + + async def test_energy_sensors( hass: HomeAssistant, hank_binary_switch, integration ) -> None: @@ -424,10 +458,7 @@ async def test_node_status_sensor_not_ready( async def test_reset_meter( - hass: HomeAssistant, - client, - aeon_smart_switch_6, - integration, + hass: HomeAssistant, client, aeon_smart_switch_6, integration ) -> None: """Test reset_meter service.""" client.async_send_command.return_value = {} @@ -487,10 +518,7 @@ async def test_reset_meter( async def test_meter_attributes( - hass: HomeAssistant, - client, - aeon_smart_switch_6, - integration, + hass: HomeAssistant, client, aeon_smart_switch_6, integration ) -> None: """Test meter entity attributes.""" state = hass.states.get(METER_ENERGY_SENSOR) @@ -501,6 +529,42 @@ async def test_meter_attributes( assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL_INCREASING +async def test_invalid_meter_scale( + hass: HomeAssistant, client, aeon_smart_switch_6_state, integration +) -> None: + """Test a meter sensor with an invalid scale.""" + node_state = copy.deepcopy(aeon_smart_switch_6_state) + value = next( + value + for value in node_state["values"] + if value["commandClass"] == 50 + and value["property"] == "value" + and value["propertyKey"] == 65537 + ) + value["metadata"]["ccSpecific"]["scale"] = -1 + value["metadata"]["unit"] = None + + event = Event( + "node added", + { + "source": "controller", + "event": "node added", + "node": node_state, + "result": "", + }, + ) + client.driver.controller.receive_event(event) + await hass.async_block_till_done() + + state = hass.states.get(METER_ENERGY_SENSOR) + assert state + assert state.attributes[ATTR_METER_TYPE] == MeterType.ELECTRIC.value + assert state.attributes[ATTR_METER_TYPE_NAME] == MeterType.ELECTRIC.name + assert ATTR_DEVICE_CLASS not in state.attributes + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + + async def test_special_meters( hass: HomeAssistant, aeon_smart_switch_6_state, client, integration ) -> None: