From 58ad3b61f75ff3dbdedf20fdabc8f5b435c0e498 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 27 Apr 2021 08:43:06 +0200 Subject: [PATCH] Entities for secondary temperature values created by certain Xiaomi devices in deCONZ (#49724) * Create sensors for secondary temperature values created by certain Xiaomi devices * Fix tests --- homeassistant/components/deconz/sensor.py | 47 +++++++++++++++++++ tests/components/deconz/test_binary_sensor.py | 20 ++++++-- tests/components/deconz/test_sensor.py | 30 +++++++++--- 3 files changed, 87 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 92686892d6a..ba3be37da42 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -114,6 +114,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ): entities.append(DeconzSensor(sensor, gateway)) + if sensor.secondary_temperature: + known_temperature_sensors = set(gateway.entities[DOMAIN]) + new_temperature_sensor = DeconzTemperature(sensor, gateway) + if new_temperature_sensor.unique_id not in known_temperature_sensors: + entities.append(new_temperature_sensor) + if entities: async_add_entities(entities) @@ -192,6 +198,47 @@ class DeconzSensor(DeconzDevice, SensorEntity): return attr +class DeconzTemperature(DeconzDevice, SensorEntity): + """Representation of a deCONZ temperature sensor. + + Extra temperature sensor on certain Xiaomi devices. + """ + + TYPE = DOMAIN + + @property + def unique_id(self): + """Return a unique identifier for this device.""" + return f"{self.serial}-temperature" + + @callback + def async_update_callback(self, force_update=False): + """Update the sensor's state.""" + keys = {"temperature", "reachable"} + if force_update or self._device.changed_keys.intersection(keys): + super().async_update_callback(force_update=force_update) + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.secondary_temperature + + @property + def name(self): + """Return the name of the temperature sensor.""" + return f"{self._device.name} Temperature" + + @property + def device_class(self): + """Return the class of the sensor.""" + return DEVICE_CLASS_TEMPERATURE + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this sensor.""" + return TEMP_CELSIUS + + class DeconzBattery(DeconzDevice, SensorEntity): """Battery class for when a device is only represented as an event.""" diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 9d4c86ead6c..6ba79dfe4ab 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -13,7 +13,13 @@ from homeassistant.components.deconz.const import ( DOMAIN as DECONZ_DOMAIN, ) from homeassistant.components.deconz.services import SERVICE_DEVICE_REFRESH -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + DEVICE_CLASS_TEMPERATURE, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_registry import async_entries_for_config_entry @@ -72,15 +78,21 @@ async def test_binary_sensors(hass, aioclient_mock, mock_deconz_websocket): with patch.dict(DECONZ_WEB_REQUEST, data): config_entry = await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_all()) == 5 presence_sensor = hass.states.get("binary_sensor.presence_sensor") assert presence_sensor.state == STATE_OFF - assert presence_sensor.attributes["device_class"] == DEVICE_CLASS_MOTION + assert presence_sensor.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MOTION + presence_temp = hass.states.get("sensor.presence_sensor_temperature") + assert presence_temp.state == "0.1" + assert presence_temp.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE assert hass.states.get("binary_sensor.temperature_sensor") is None assert hass.states.get("binary_sensor.clip_presence_sensor") is None vibration_sensor = hass.states.get("binary_sensor.vibration_sensor") assert vibration_sensor.state == STATE_ON - assert vibration_sensor.attributes["device_class"] == DEVICE_CLASS_VIBRATION + assert vibration_sensor.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_VIBRATION + vibration_temp = hass.states.get("sensor.vibration_sensor_temperature") + assert vibration_temp.state == "0.1" + assert vibration_temp.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE event_changed_sensor = { "t": "event", diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index a4d4e006366..d9c4adf1388 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -89,13 +90,17 @@ async def test_sensors(hass, aioclient_mock, mock_deconz_websocket): with patch.dict(DECONZ_WEB_REQUEST, data): config_entry = await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 5 + assert len(hass.states.async_all()) == 6 light_level_sensor = hass.states.get("sensor.light_level_sensor") assert light_level_sensor.state == "999.8" assert light_level_sensor.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ILLUMINANCE assert light_level_sensor.attributes[ATTR_DAYLIGHT] == 6955 + light_level_temp = hass.states.get("sensor.light_level_sensor_temperature") + assert light_level_temp.state == "0.1" + assert light_level_temp.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE + assert not hass.states.get("sensor.presence_sensor") assert not hass.states.get("sensor.switch_1") assert not hass.states.get("sensor.switch_1_battery_level") @@ -130,6 +135,19 @@ async def test_sensors(hass, aioclient_mock, mock_deconz_websocket): assert hass.states.get("sensor.light_level_sensor").state == "1.6" + # Event signals new temperature value + + event_changed_sensor = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "config": {"temperature": 100}, + } + await mock_deconz_websocket(data=event_changed_sensor) + + assert hass.states.get("sensor.light_level_sensor_temperature").state == "1.0" + # Event signals new battery level event_changed_sensor = { @@ -148,7 +166,7 @@ async def test_sensors(hass, aioclient_mock, mock_deconz_websocket): await hass.config_entries.async_unload(config_entry.entry_id) states = hass.states.async_all() - assert len(states) == 5 + assert len(states) == 6 for state in states: assert state.state == STATE_UNAVAILABLE @@ -187,7 +205,7 @@ async def test_allow_clip_sensors(hass, aioclient_mock): options={CONF_ALLOW_CLIP_SENSOR: True}, ) - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_all()) == 3 assert hass.states.get("sensor.clip_light_level_sensor").state == "999.8" # Disallow clip sensors @@ -197,7 +215,7 @@ async def test_allow_clip_sensors(hass, aioclient_mock): ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all()) == 2 assert not hass.states.get("sensor.clip_light_level_sensor") # Allow clip sensors @@ -207,7 +225,7 @@ async def test_allow_clip_sensors(hass, aioclient_mock): ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_all()) == 3 assert hass.states.get("sensor.clip_light_level_sensor").state == "999.8" @@ -235,7 +253,7 @@ async def test_add_new_sensor(hass, aioclient_mock, mock_deconz_websocket): await mock_deconz_websocket(data=event_added_sensor) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all()) == 2 assert hass.states.get("sensor.light_level_sensor").state == "999.8"