From 008dddb17c8cf3fc79488ea2562e0d5a78db5d91 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 10 Jan 2020 20:30:58 -0500 Subject: [PATCH] Fix ZHA temperature sensor restoration (#30661) * Add test for restoring state for zha temp. * Don't restore unit of measurement for ZHA sensors. Properly restore ZHA temperature sensor state. --- homeassistant/components/zha/sensor.py | 20 +++- tests/components/zha/test_sensor.py | 123 +++++++++++++++++++++++-- 2 files changed, 135 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 2d39d562bf5..3b73a9793c9 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -12,9 +12,15 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_TEMPERATURE, DOMAIN, ) -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, POWER_WATT, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + POWER_WATT, + STATE_UNKNOWN, + TEMP_CELSIUS, +) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.temperature import fahrenheit_to_celsius from .core.const import ( CHANNEL_ELECTRICAL_MEASUREMENT, @@ -160,7 +166,6 @@ class Sensor(ZhaEntity): def async_restore_last_state(self, last_state): """Restore previous state.""" self._state = last_state.state - self._unit = last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @callback async def async_state_attr_provider(self): @@ -277,3 +282,14 @@ class Temperature(Sensor): _device_class = DEVICE_CLASS_TEMPERATURE _divisor = 100 _unit = TEMP_CELSIUS + + @callback + def async_restore_last_state(self, last_state): + """Restore previous state.""" + if last_state.state == STATE_UNKNOWN: + return + if last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) != TEMP_CELSIUS: + ftemp = float(last_state.state) + self._state = round(fahrenheit_to_celsius(ftemp), 1) + return + self._state = last_state.state diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index b2daf4da765..3e02542a4fb 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -1,4 +1,5 @@ """Test zha sensor.""" +import pytest import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.homeautomation as homeautomation import zigpy.zcl.clusters.measurement as measurement @@ -6,7 +7,20 @@ import zigpy.zcl.clusters.smartenergy as smartenergy import zigpy.zcl.foundation as zcl_f from homeassistant.components.sensor import DOMAIN -from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN +import homeassistant.config as config_util +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_UNIT_OF_MEASUREMENT, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.helpers import restore_state +from homeassistant.util import dt as dt_util from .common import ( async_enable_traffic, @@ -39,7 +53,7 @@ async def test_sensor(hass, config_entry, zha_gateway): # ensure the sensor entity was created for each id in cluster_ids for cluster_id in cluster_ids: zigpy_device_info = zigpy_device_infos[cluster_id] - entity_id = zigpy_device_info["entity_id"] + entity_id = zigpy_device_info[ATTR_ENTITY_ID] assert hass.states.get(entity_id).state == STATE_UNAVAILABLE # allow traffic to flow through the gateway and devices @@ -55,7 +69,7 @@ async def test_sensor(hass, config_entry, zha_gateway): # test that the sensors now have a state of unknown for cluster_id in cluster_ids: zigpy_device_info = zigpy_device_infos[cluster_id] - entity_id = zigpy_device_info["entity_id"] + entity_id = zigpy_device_info[ATTR_ENTITY_ID] assert hass.states.get(entity_id).state == STATE_UNKNOWN # get the humidity device info and test the associated sensor logic @@ -128,7 +142,7 @@ async def async_build_devices(hass, zha_gateway, config_entry, cluster_ids): device_info["cluster"] = zigpy_device.endpoints.get(1).in_clusters[cluster_id] zha_device = zha_gateway.get_device(zigpy_device.ieee) device_info["zha_device"] = zha_device - device_info["entity_id"] = await find_entity_id(DOMAIN, zha_device, hass) + device_info[ATTR_ENTITY_ID] = await find_entity_id(DOMAIN, zha_device, hass) await hass.async_block_till_done() return device_infos @@ -187,6 +201,103 @@ def assert_state(hass, device_info, state, unit_of_measurement): This is used to ensure that the logic in each sensor class handled the attribute report it received correctly. """ - hass_state = hass.states.get(device_info["entity_id"]) + hass_state = hass.states.get(device_info[ATTR_ENTITY_ID]) assert hass_state.state == state - assert hass_state.attributes.get("unit_of_measurement") == unit_of_measurement + assert hass_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == unit_of_measurement + + +@pytest.fixture +def hass_ms(hass): + """Hass instance with measurement system.""" + + async def _hass_ms(meas_sys): + await config_util.async_process_ha_core_config( + hass, {CONF_UNIT_SYSTEM: meas_sys} + ) + await hass.async_block_till_done() + return hass + + return _hass_ms + + +@pytest.fixture +def core_rs(hass_storage): + """Core.restore_state fixture.""" + + def _storage(entity_id, uom, state): + now = dt_util.utcnow().isoformat() + + hass_storage[restore_state.STORAGE_KEY] = { + "version": restore_state.STORAGE_VERSION, + "key": restore_state.STORAGE_KEY, + "data": [ + { + "state": { + "entity_id": entity_id, + "state": str(state), + "attributes": {ATTR_UNIT_OF_MEASUREMENT: uom}, + "last_changed": now, + "last_updated": now, + "context": { + "id": "3c2243ff5f30447eb12e7348cfd5b8ff", + "user_id": None, + }, + }, + "last_seen": now, + } + ], + } + return + + return _storage + + +@pytest.mark.parametrize( + "uom, raw_temp, expected, restore", + [ + (TEMP_CELSIUS, 2900, 29, False), + (TEMP_CELSIUS, 2900, 29, True), + (TEMP_FAHRENHEIT, 2900, 84, False), + (TEMP_FAHRENHEIT, 2900, 84, True), + ], +) +async def test_temp_uom( + uom, raw_temp, expected, restore, hass_ms, config_entry, zha_gateway, core_rs +): + """Test zha temperature sensor unit of measurement.""" + + entity_id = "sensor.fake1026_fakemodel1026_004f3202_temperature" + if restore: + core_rs(entity_id, uom, state=(expected - 2)) + + hass = await hass_ms( + CONF_UNIT_SYSTEM_METRIC if uom == TEMP_CELSIUS else CONF_UNIT_SYSTEM_IMPERIAL + ) + + # list of cluster ids to create devices and sensor entities for + temp_cluster = measurement.TemperatureMeasurement + cluster_ids = [temp_cluster.cluster_id] + + # devices that were created from cluster_ids list above + zigpy_device_infos = await async_build_devices( + hass, zha_gateway, config_entry, cluster_ids + ) + + zigpy_device_info = zigpy_device_infos[temp_cluster.cluster_id] + zha_device = zigpy_device_info["zha_device"] + if not restore: + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + # allow traffic to flow through the gateway and devices + await async_enable_traffic(hass, zha_gateway, [zha_device]) + + # test that the sensors now have a state of unknown + if not restore: + assert hass.states.get(entity_id).state == STATE_UNKNOWN + + await send_attribute_report(hass, zigpy_device_info["cluster"], 0, raw_temp) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state is not None + assert round(float(state.state)) == expected + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == uom